Selectively enable user site

The modern virtual environment structure does not allow us to enable
"fake user site" while disabling the global site, so we need to do more
fine-grained configuration to correctly set up test environments for
each test case.

With this done, we can also properly support the stdlib venv ad the test
environment backend, since it basically works identically with modern
virtualenv. The incompatible_with_test_venv is thus removed.
This commit is contained in:
Tzu-ping Chung 2022-10-27 21:34:17 +08:00
parent 83c85e94b7
commit 50e194f107
12 changed files with 70 additions and 78 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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.

View File

@ -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:

View File

@ -35,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:
@ -57,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:
@ -77,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
@ -123,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:
@ -148,7 +145,6 @@ class Tests_UserSite:
result2.did_create(egg_info_folder)
assert not isfile(initools_v3_file), initools_v3_file
@pytest.mark.incompatible_with_test_venv
def test_install_user_conflict_in_globalsite(
self, virtualenv: VirtualEnvironment, script: PipTestEnvironment
) -> None:
@ -191,7 +187,6 @@ class Tests_UserSite:
assert isdir(dist_info_folder)
assert isdir(initools_folder)
@pytest.mark.incompatible_with_test_venv
def test_upgrade_user_conflict_in_globalsite(
self, virtualenv: VirtualEnvironment, script: PipTestEnvironment
) -> None:
@ -235,7 +230,6 @@ class Tests_UserSite:
assert isdir(dist_info_folder), result2.stdout
assert isdir(initools_folder)
@pytest.mark.incompatible_with_test_venv
def test_install_user_conflict_in_globalsite_and_usersite(
self, virtualenv: VirtualEnvironment, script: PipTestEnvironment
) -> None:
@ -294,7 +288,6 @@ class Tests_UserSite:
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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -11,7 +11,7 @@ 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:

View File

@ -20,9 +20,6 @@ else:
VirtualEnvironmentType = str
LEGACY_VIRTUALENV = int(_virtualenv.__version__.split(".", 1)[0]) < 20
class VirtualEnvironment:
"""
An abstraction around virtual environments, currently it only uses
@ -34,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
@ -44,13 +41,18 @@ class VirtualEnvironment:
self._venv_type = venv_type
else:
self._venv_type = "virtualenv"
assert self._venv_type in ("virtualenv", "venv")
self._user_site_packages = False
self._template = template
self._sitecustomize: Optional[str] = None
self._update_paths()
self._create()
@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)
@ -63,7 +65,7 @@ class VirtualEnvironment:
self.lib = Path(lib)
def _update_paths(self) -> None:
if LEGACY_VIRTUALENV:
if self._legacy_virtualenv:
self.__update_paths_legacy()
return
bases = {
@ -86,7 +88,11 @@ class VirtualEnvironment:
if self._template:
# On Windows, calling `_virtualenv.path_locations(target)`
# will have created the `target` directory...
if LEGACY_VIRTUALENV and 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)
@ -94,35 +100,36 @@ class VirtualEnvironment:
self._user_site_packages = self._template.user_site_packages
else:
# Create a new virtual environment.
if self._venv_type == "virtualenv":
if LEGACY_VIRTUALENV:
subprocess.check_call(
[
sys.executable,
"-m",
"virtualenv",
"--no-pip",
"--no-wheel",
"--no-setuptools",
os.fspath(self.location),
]
)
self._fix_legacy_virtualenv_site_module()
else:
_virtualenv.cli_run(
[
"--no-pip",
"--no-wheel",
"--no-setuptools",
os.fspath(self.location),
],
)
if self._legacy_virtualenv:
subprocess.check_call(
[
sys.executable,
"-m",
"virtualenv",
"--no-pip",
"--no-wheel",
"--no-setuptools",
os.fspath(self.location),
]
)
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
@ -161,7 +168,9 @@ class VirtualEnvironment:
assert compileall.compile_file(str(site_py), quiet=1, force=True)
def _customize_site(self) -> None:
if not LEGACY_VIRTUALENV or self._venv_type == "venv":
if self._legacy_virtualenv:
contents = ""
else:
# Enable user site (before system).
contents = textwrap.dedent(
f"""
@ -186,8 +195,6 @@ class VirtualEnvironment:
site.addsitedir(path)
"""
).strip()
else:
contents = ""
if self._sitecustomize is not None:
contents += "\n" + self._sitecustomize
sitecustomize = self.site / "sitecustomize.py"
@ -234,14 +241,14 @@ class VirtualEnvironment:
@user_site_packages.setter
def user_site_packages(self, value: bool) -> None:
self._user_site_packages = value
if not LEGACY_VIRTUALENV or self._venv_type == "venv":
self._rewrite_pyvenv_cfg(
{"include-system-site-packages": str(bool(value)).lower()}
)
self._customize_site()
else:
if self._legacy_virtualenv:
marker = self.lib / "no-global-site-packages.txt"
if self._user_site_packages:
marker.unlink()
else:
marker.touch()
else:
self._rewrite_pyvenv_cfg(
{"include-system-site-packages": str(bool(value)).lower()}
)
self._customize_site()