From a57668ef12da8151a58e2438c23130a376265c37 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Mon, 11 Jul 2022 16:26:24 +0100
Subject: [PATCH 1/9] Add an option to the test suite to specify a zipapp to
test
---
tests/conftest.py | 16 +++++++++++++---
tests/lib/__init__.py | 9 ++++++++-
2 files changed, 21 insertions(+), 4 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index 1cf058d7000..096ef2c898a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -84,6 +84,12 @@ def pytest_addoption(parser: Parser) -> None:
default=None,
help="use given proxy in session network tests",
)
+ parser.addoption(
+ "--use-zipapp",
+ action="store",
+ default=None,
+ help="use given pip zipapp when running pip in tests",
+ )
def pytest_collection_modifyitems(config: Config, items: List[pytest.Function]) -> None:
@@ -487,7 +493,7 @@ def with_wheel(virtualenv: VirtualEnvironment, wheel_install: Path) -> None:
class ScriptFactory(Protocol):
def __call__(
- self, tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None
+ self, tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None, zipapp: Optional[str] = None
) -> PipTestEnvironment:
...
@@ -497,7 +503,7 @@ def script_factory(
virtualenv_factory: Callable[[Path], VirtualEnvironment], deprecated_python: bool
) -> ScriptFactory:
def factory(
- tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None
+ tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None, zipapp: Optional[str] = None,
) -> PipTestEnvironment:
if virtualenv is None:
virtualenv = virtualenv_factory(tmpdir.joinpath("venv"))
@@ -516,6 +522,8 @@ def factory(
assert_no_temp=True,
# Deprecated python versions produce an extra deprecation warning
pip_expect_warning=deprecated_python,
+ # Tell the Test Environment if we want to run pip via a zipapp
+ zipapp=zipapp,
)
return factory
@@ -523,6 +531,7 @@ def factory(
@pytest.fixture
def script(
+ request: pytest.FixtureRequest,
tmpdir: Path,
virtualenv: VirtualEnvironment,
script_factory: ScriptFactory,
@@ -533,7 +542,8 @@ def script(
test function. The returned object is a
``tests.lib.PipTestEnvironment``.
"""
- return script_factory(tmpdir.joinpath("workspace"), virtualenv)
+ zipapp = request.config.getoption("--use-zipapp")
+ return script_factory(tmpdir.joinpath("workspace"), virtualenv, zipapp)
@pytest.fixture(scope="session")
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index 43624c16614..2750a552b6e 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -507,6 +507,7 @@ def __init__(
*args: Any,
virtualenv: VirtualEnvironment,
pip_expect_warning: bool = False,
+ zipapp: Optional[str] = None,
**kwargs: Any,
) -> None:
# Store paths related to the virtual environment
@@ -553,6 +554,9 @@ def __init__(
# (useful for Python version deprecation)
self.pip_expect_warning = pip_expect_warning
+ # The name of an (optional) zipapp to use when running pip
+ self.zipapp = zipapp
+
# Call the TestFileEnvironment __init__
super().__init__(base_path, *args, **kwargs)
@@ -698,7 +702,10 @@ def pip(
__tracebackhide__ = True
if self.pip_expect_warning:
kwargs["allow_stderr_warning"] = True
- if use_module:
+ if self.zipapp:
+ exe = "python"
+ args = (self.zipapp, ) + args
+ elif use_module:
exe = "python"
args = ("-m", "pip") + args
else:
From ef999f4c7668339a8772f75aab67c7e286673493 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Mon, 11 Jul 2022 17:18:21 +0100
Subject: [PATCH 2/9] Ignore temporary extracted copies of cacert.pem when
testing with a zipapp
---
tests/lib/__init__.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index 2750a552b6e..a8d74b6d44c 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -591,6 +591,10 @@ def __init__(
def _ignore_file(self, fn: str) -> bool:
if fn.endswith("__pycache__") or fn.endswith(".pyc"):
result = True
+ elif self.zipapp and fn.endswith("cacert.pem"):
+ # Temporary copies of cacert.pem are extracted
+ # when running from a zipapp
+ result = True
else:
result = super()._ignore_file(fn)
return result
From 9a51fc8e0c58e8d93f33141d9c1e5e154ebedd51 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Mon, 11 Jul 2022 20:01:26 +0100
Subject: [PATCH 3/9] Make the zipapp in a fixture
---
tests/conftest.py | 38 ++++++++++++++++++++++++++++++--------
1 file changed, 30 insertions(+), 8 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index 096ef2c898a..0cb047625dc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -86,9 +86,9 @@ def pytest_addoption(parser: Parser) -> None:
)
parser.addoption(
"--use-zipapp",
- action="store",
- default=None,
- help="use given pip zipapp when running pip in tests",
+ action="store_true",
+ default=False,
+ help="use a zipapp when running pip in tests",
)
@@ -493,17 +493,17 @@ def with_wheel(virtualenv: VirtualEnvironment, wheel_install: Path) -> None:
class ScriptFactory(Protocol):
def __call__(
- self, tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None, zipapp: Optional[str] = None
+ self, tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None
) -> PipTestEnvironment:
...
@pytest.fixture(scope="session")
def script_factory(
- virtualenv_factory: Callable[[Path], VirtualEnvironment], deprecated_python: bool
+ virtualenv_factory: Callable[[Path], VirtualEnvironment], deprecated_python: bool, zipapp: Optional[str]
) -> ScriptFactory:
def factory(
- tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None, zipapp: Optional[str] = None,
+ tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None,
) -> PipTestEnvironment:
if virtualenv is None:
virtualenv = virtualenv_factory(tmpdir.joinpath("venv"))
@@ -529,6 +529,29 @@ def factory(
return factory
+@pytest.fixture(scope="session")
+def zipapp(request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory) -> Optional[str]:
+ """
+ If the user requested for pip to be run from a zipapp, build that zipapp
+ and return its location. If the user didn't request a zipapp, return None.
+
+ This fixture is session scoped, so the zipapp will only be created once.
+ """
+ if not request.config.getoption("--use-zipapp"):
+ return None
+
+ temp_location = tmpdir_factory.mktemp("zipapp")
+ pyz_file = temp_location / "pip.pyz"
+ # What we want to do here is `pip wheel --wheel-dir temp_location `
+ # and then build a zipapp from that wheel.
+ # TODO: Remove hard coded file
+ za = "pip-22.2.dev0.pyz"
+ import warnings
+ warnings.warn(f"Copying {za} to {pyz_file}")
+ shutil.copyfile(za, pyz_file)
+ return str(pyz_file)
+
+
@pytest.fixture
def script(
request: pytest.FixtureRequest,
@@ -542,8 +565,7 @@ def script(
test function. The returned object is a
``tests.lib.PipTestEnvironment``.
"""
- zipapp = request.config.getoption("--use-zipapp")
- return script_factory(tmpdir.joinpath("workspace"), virtualenv, zipapp)
+ return script_factory(tmpdir.joinpath("workspace"), virtualenv)
@pytest.fixture(scope="session")
From b84e5f3d9976241400e56b83b40a5c4e4c40294c Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Mon, 11 Jul 2022 23:52:44 +0100
Subject: [PATCH 4/9] Actually build the zipapp
---
tests/conftest.py | 39 ++++++++++++++++++++++++++++++++-------
1 file changed, 32 insertions(+), 7 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index 0cb047625dc..aff7390f6e8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -20,6 +20,7 @@
Union,
)
from unittest.mock import patch
+from zipfile import ZipFile
import pytest
@@ -32,6 +33,7 @@
from _pytest.config.argparsing import Parser
from setuptools.wheel import Wheel
+from pip import __file__ as pip_location
from pip._internal.cli.main import main as pip_entry_point
from pip._internal.locations import _USE_SYSCONFIG
from pip._internal.utils.temp_dir import global_tempdir_manager
@@ -529,6 +531,35 @@ def factory(
return factory
+ZIPAPP_MAIN = """\
+#!/usr/bin/env python
+
+import os
+import runpy
+import sys
+
+lib = os.path.join(os.path.dirname(__file__), "lib")
+sys.path.insert(0, lib)
+
+runpy.run_module("pip", run_name="__main__")
+"""
+
+def make_zipapp_from_pip(zipapp_name: Path) -> None:
+ pip_dir = Path(pip_location).parent
+ with zipapp_name.open("wb") as zipapp_file:
+ zipapp_file.write(b"#!/usr/bin/env python\n")
+ with ZipFile(zipapp_file, "w") as zipapp:
+ for pip_file in pip_dir.rglob("*"):
+ if pip_file.suffix == ".pyc":
+ continue
+ if pip_file.name == "__pycache__":
+ continue
+ rel_name = pip_file.relative_to(pip_dir.parent)
+ zipapp.write(pip_file, arcname=f"lib/{rel_name}")
+ zipapp.writestr("__main__.py", ZIPAPP_MAIN)
+
+
+
@pytest.fixture(scope="session")
def zipapp(request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory) -> Optional[str]:
"""
@@ -542,13 +573,7 @@ def zipapp(request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactor
temp_location = tmpdir_factory.mktemp("zipapp")
pyz_file = temp_location / "pip.pyz"
- # What we want to do here is `pip wheel --wheel-dir temp_location `
- # and then build a zipapp from that wheel.
- # TODO: Remove hard coded file
- za = "pip-22.2.dev0.pyz"
- import warnings
- warnings.warn(f"Copying {za} to {pyz_file}")
- shutil.copyfile(za, pyz_file)
+ make_zipapp_from_pip(pyz_file)
return str(pyz_file)
From c7e7e426cb2a53127bae11492590f883db1779f4 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Tue, 12 Jul 2022 09:02:11 +0100
Subject: [PATCH 5/9] Apply black
---
tests/conftest.py | 13 +++++++++----
tests/lib/__init__.py | 2 +-
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/tests/conftest.py b/tests/conftest.py
index aff7390f6e8..0523bdc20a3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -502,10 +502,13 @@ def __call__(
@pytest.fixture(scope="session")
def script_factory(
- virtualenv_factory: Callable[[Path], VirtualEnvironment], deprecated_python: bool, zipapp: Optional[str]
+ virtualenv_factory: Callable[[Path], VirtualEnvironment],
+ deprecated_python: bool,
+ zipapp: Optional[str],
) -> ScriptFactory:
def factory(
- tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None,
+ tmpdir: Path,
+ virtualenv: Optional[VirtualEnvironment] = None,
) -> PipTestEnvironment:
if virtualenv is None:
virtualenv = virtualenv_factory(tmpdir.joinpath("venv"))
@@ -544,6 +547,7 @@ def factory(
runpy.run_module("pip", run_name="__main__")
"""
+
def make_zipapp_from_pip(zipapp_name: Path) -> None:
pip_dir = Path(pip_location).parent
with zipapp_name.open("wb") as zipapp_file:
@@ -559,9 +563,10 @@ def make_zipapp_from_pip(zipapp_name: Path) -> None:
zipapp.writestr("__main__.py", ZIPAPP_MAIN)
-
@pytest.fixture(scope="session")
-def zipapp(request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory) -> Optional[str]:
+def zipapp(
+ request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory
+) -> Optional[str]:
"""
If the user requested for pip to be run from a zipapp, build that zipapp
and return its location. If the user didn't request a zipapp, return None.
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index a8d74b6d44c..be3e4c36e9a 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -708,7 +708,7 @@ def pip(
kwargs["allow_stderr_warning"] = True
if self.zipapp:
exe = "python"
- args = (self.zipapp, ) + args
+ args = (self.zipapp,) + args
elif use_module:
exe = "python"
args = ("-m", "pip") + args
From ea2318fbf9857834b2cc68dac960bc1536875733 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Tue, 12 Jul 2022 10:12:17 +0100
Subject: [PATCH 6/9] Minor zipapp-related fixes and skips for some tests
---
tests/functional/test_cli.py | 3 +++
tests/functional/test_completion.py | 7 ++++++-
tests/lib/test_lib.py | 4 ++++
3 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py
index 3e8570359bb..a1b69b72106 100644
--- a/tests/functional/test_cli.py
+++ b/tests/functional/test_cli.py
@@ -16,6 +16,9 @@
],
)
def test_entrypoints_work(entrypoint: str, script: PipTestEnvironment) -> None:
+ if script.zipapp:
+ pytest.skip("Zipapp does not include entrypoints")
+
fake_pkg = script.temp_path / "fake_pkg"
fake_pkg.mkdir()
fake_pkg.joinpath("setup.py").write_text(
diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py
index df4afab74b8..b02cd4fa317 100644
--- a/tests/functional/test_completion.py
+++ b/tests/functional/test_completion.py
@@ -107,7 +107,12 @@ def test_completion_for_supported_shells(
Test getting completion for bash shell
"""
result = script_with_launchers.pip("completion", "--" + shell, use_module=False)
- assert completion in result.stdout, str(result.stdout)
+ actual = str(result.stdout)
+ if script_with_launchers.zipapp:
+ # The zipapp reports its name as "pip.pyz", but the expected
+ # output assumes "pip"
+ actual = actual.replace("pip.pyz", "pip")
+ assert completion in actual, actual
@pytest.fixture(scope="session")
diff --git a/tests/lib/test_lib.py b/tests/lib/test_lib.py
index 99514d5f92c..ea9baed54d3 100644
--- a/tests/lib/test_lib.py
+++ b/tests/lib/test_lib.py
@@ -41,6 +41,10 @@ def test_correct_pip_version(script: PipTestEnvironment) -> None:
"""
Check we are running proper version of pip in run_pip.
"""
+
+ if script.zipapp:
+ pytest.skip("Test relies on the pip under test being in the filesystem")
+
# output is like:
# pip PIPVERSION from PIPDIRECTORY (python PYVERSION)
result = script.pip("--version")
From f7240d8691ee99cab6a77da13a7e43c717f6eab3 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Tue, 12 Jul 2022 10:27:52 +0100
Subject: [PATCH 7/9] Add a news file
---
news/11250.feature.rst | 1 +
1 file changed, 1 insertion(+)
create mode 100644 news/11250.feature.rst
diff --git a/news/11250.feature.rst b/news/11250.feature.rst
new file mode 100644
index 00000000000..a80c54699c8
--- /dev/null
+++ b/news/11250.feature.rst
@@ -0,0 +1 @@
+Add an option to run the test suite with pip built as a zipapp.
From 81e813ac7948e9b3af7e0f3b3555405652dc7963 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Tue, 12 Jul 2022 10:28:34 +0100
Subject: [PATCH 8/9] Add testing with pip built as a zipapp to the CI
---
.github/workflows/ci.yml | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e467b3e50b1..439b6fabb52 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -219,6 +219,35 @@ jobs:
env:
TEMP: "R:\\Temp"
+ tests-zipapp:
+ name: tests / zipapp
+ runs-on: ubuntu-latest
+
+ needs: [pre-commit, packaging, determine-changes]
+ if: >-
+ needs.determine-changes.outputs.tests == 'true' ||
+ github.event_name != 'pull_request'
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: "3.10"
+
+ - name: Install Ubuntu dependencies
+ run: sudo apt-get install bzr
+
+ - run: pip install nox 'virtualenv<20' 'setuptools != 60.6.0'
+
+ # Main check
+ - name: Run integration tests
+ run: >-
+ nox -s test-3.10 --
+ -m integration
+ --verbose --numprocesses auto --showlocals
+ --durations=5
+ --use-zipapp
+
# TODO: Remove this when we add Python 3.11 to CI.
tests-importlib-metadata:
name: tests for importlib.metadata backend
From ee6c7caabdfcd0bddd9b92d05cddd8b6be7cbe10 Mon Sep 17 00:00:00 2001
From: Paul Moore
Date: Thu, 28 Jul 2022 11:30:54 +0100
Subject: [PATCH 9/9] Fix test_runner_work_in_environments_with_no_pip to work
under --use-zipapp
---
tests/functional/test_pip_runner_script.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/tests/functional/test_pip_runner_script.py b/tests/functional/test_pip_runner_script.py
index 26016d45a08..f2f879b824d 100644
--- a/tests/functional/test_pip_runner_script.py
+++ b/tests/functional/test_pip_runner_script.py
@@ -12,7 +12,9 @@ def test_runner_work_in_environments_with_no_pip(
# Ensure there's no pip installed in the environment
script.pip("uninstall", "pip", "--yes", use_module=True)
- script.pip("--version", expect_error=True)
+ # We don't use script.pip to check here, as when testing a
+ # zipapp, script.pip will run pip from the zipapp.
+ script.run("python", "-c", "import pip", expect_error=True)
# The runner script should still invoke a usable pip
result = script.run("python", os.fspath(runner), "--version")