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")