Skip to content

Commit f47a204

Browse files
authored
Merge pull request #11250 from pfmoore/test_zipapp
Add a --use-zipapp option to the test suite
2 parents 4b95250 + ee6c7ca commit f47a204

File tree

8 files changed

+122
-5
lines changed

8 files changed

+122
-5
lines changed

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,35 @@ jobs:
221221
env:
222222
TEMP: "R:\\Temp"
223223

224+
tests-zipapp:
225+
name: tests / zipapp
226+
runs-on: ubuntu-latest
227+
228+
needs: [pre-commit, packaging, determine-changes]
229+
if: >-
230+
needs.determine-changes.outputs.tests == 'true' ||
231+
github.event_name != 'pull_request'
232+
233+
steps:
234+
- uses: actions/checkout@v2
235+
- uses: actions/setup-python@v2
236+
with:
237+
python-version: "3.10"
238+
239+
- name: Install Ubuntu dependencies
240+
run: sudo apt-get install bzr
241+
242+
- run: pip install nox 'virtualenv<20' 'setuptools != 60.6.0'
243+
244+
# Main check
245+
- name: Run integration tests
246+
run: >-
247+
nox -s test-3.10 --
248+
-m integration
249+
--verbose --numprocesses auto --showlocals
250+
--durations=5
251+
--use-zipapp
252+
224253
# TODO: Remove this when we add Python 3.11 to CI.
225254
tests-importlib-metadata:
226255
name: tests for importlib.metadata backend

news/11250.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add an option to run the test suite with pip built as a zipapp.

tests/conftest.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Union,
2020
)
2121
from unittest.mock import patch
22+
from zipfile import ZipFile
2223

2324
import pytest
2425

@@ -33,6 +34,7 @@
3334
from installer.destinations import SchemeDictionaryDestination
3435
from installer.sources import WheelFile
3536

37+
from pip import __file__ as pip_location
3638
from pip._internal.cli.main import main as pip_entry_point
3739
from pip._internal.locations import _USE_SYSCONFIG
3840
from pip._internal.utils.temp_dir import global_tempdir_manager
@@ -85,6 +87,12 @@ def pytest_addoption(parser: Parser) -> None:
8587
default=None,
8688
help="use given proxy in session network tests",
8789
)
90+
parser.addoption(
91+
"--use-zipapp",
92+
action="store_true",
93+
default=False,
94+
help="use a zipapp when running pip in tests",
95+
)
8896

8997

9098
def pytest_collection_modifyitems(config: Config, items: List[pytest.Function]) -> None:
@@ -513,10 +521,13 @@ def __call__(
513521

514522
@pytest.fixture(scope="session")
515523
def script_factory(
516-
virtualenv_factory: Callable[[Path], VirtualEnvironment], deprecated_python: bool
524+
virtualenv_factory: Callable[[Path], VirtualEnvironment],
525+
deprecated_python: bool,
526+
zipapp: Optional[str],
517527
) -> ScriptFactory:
518528
def factory(
519-
tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None
529+
tmpdir: Path,
530+
virtualenv: Optional[VirtualEnvironment] = None,
520531
) -> PipTestEnvironment:
521532
if virtualenv is None:
522533
virtualenv = virtualenv_factory(tmpdir.joinpath("venv"))
@@ -535,13 +546,64 @@ def factory(
535546
assert_no_temp=True,
536547
# Deprecated python versions produce an extra deprecation warning
537548
pip_expect_warning=deprecated_python,
549+
# Tell the Test Environment if we want to run pip via a zipapp
550+
zipapp=zipapp,
538551
)
539552

540553
return factory
541554

542555

556+
ZIPAPP_MAIN = """\
557+
#!/usr/bin/env python
558+
559+
import os
560+
import runpy
561+
import sys
562+
563+
lib = os.path.join(os.path.dirname(__file__), "lib")
564+
sys.path.insert(0, lib)
565+
566+
runpy.run_module("pip", run_name="__main__")
567+
"""
568+
569+
570+
def make_zipapp_from_pip(zipapp_name: Path) -> None:
571+
pip_dir = Path(pip_location).parent
572+
with zipapp_name.open("wb") as zipapp_file:
573+
zipapp_file.write(b"#!/usr/bin/env python\n")
574+
with ZipFile(zipapp_file, "w") as zipapp:
575+
for pip_file in pip_dir.rglob("*"):
576+
if pip_file.suffix == ".pyc":
577+
continue
578+
if pip_file.name == "__pycache__":
579+
continue
580+
rel_name = pip_file.relative_to(pip_dir.parent)
581+
zipapp.write(pip_file, arcname=f"lib/{rel_name}")
582+
zipapp.writestr("__main__.py", ZIPAPP_MAIN)
583+
584+
585+
@pytest.fixture(scope="session")
586+
def zipapp(
587+
request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory
588+
) -> Optional[str]:
589+
"""
590+
If the user requested for pip to be run from a zipapp, build that zipapp
591+
and return its location. If the user didn't request a zipapp, return None.
592+
593+
This fixture is session scoped, so the zipapp will only be created once.
594+
"""
595+
if not request.config.getoption("--use-zipapp"):
596+
return None
597+
598+
temp_location = tmpdir_factory.mktemp("zipapp")
599+
pyz_file = temp_location / "pip.pyz"
600+
make_zipapp_from_pip(pyz_file)
601+
return str(pyz_file)
602+
603+
543604
@pytest.fixture
544605
def script(
606+
request: pytest.FixtureRequest,
545607
tmpdir: Path,
546608
virtualenv: VirtualEnvironment,
547609
script_factory: ScriptFactory,

tests/functional/test_cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
],
1717
)
1818
def test_entrypoints_work(entrypoint: str, script: PipTestEnvironment) -> None:
19+
if script.zipapp:
20+
pytest.skip("Zipapp does not include entrypoints")
21+
1922
fake_pkg = script.temp_path / "fake_pkg"
2023
fake_pkg.mkdir()
2124
fake_pkg.joinpath("setup.py").write_text(

tests/functional/test_completion.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,12 @@ def test_completion_for_supported_shells(
107107
Test getting completion for bash shell
108108
"""
109109
result = script_with_launchers.pip("completion", "--" + shell, use_module=False)
110-
assert completion in result.stdout, str(result.stdout)
110+
actual = str(result.stdout)
111+
if script_with_launchers.zipapp:
112+
# The zipapp reports its name as "pip.pyz", but the expected
113+
# output assumes "pip"
114+
actual = actual.replace("pip.pyz", "pip")
115+
assert completion in actual, actual
111116

112117

113118
@pytest.fixture(scope="session")

tests/functional/test_pip_runner_script.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ def test_runner_work_in_environments_with_no_pip(
1212

1313
# Ensure there's no pip installed in the environment
1414
script.pip("uninstall", "pip", "--yes", use_module=True)
15-
script.pip("--version", expect_error=True)
15+
# We don't use script.pip to check here, as when testing a
16+
# zipapp, script.pip will run pip from the zipapp.
17+
script.run("python", "-c", "import pip", expect_error=True)
1618

1719
# The runner script should still invoke a usable pip
1820
result = script.run("python", os.fspath(runner), "--version")

tests/lib/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ def __init__(
505505
*args: Any,
506506
virtualenv: VirtualEnvironment,
507507
pip_expect_warning: bool = False,
508+
zipapp: Optional[str] = None,
508509
**kwargs: Any,
509510
) -> None:
510511
# Store paths related to the virtual environment
@@ -551,6 +552,9 @@ def __init__(
551552
# (useful for Python version deprecation)
552553
self.pip_expect_warning = pip_expect_warning
553554

555+
# The name of an (optional) zipapp to use when running pip
556+
self.zipapp = zipapp
557+
554558
# Call the TestFileEnvironment __init__
555559
super().__init__(base_path, *args, **kwargs)
556560

@@ -585,6 +589,10 @@ def __init__(
585589
def _ignore_file(self, fn: str) -> bool:
586590
if fn.endswith("__pycache__") or fn.endswith(".pyc"):
587591
result = True
592+
elif self.zipapp and fn.endswith("cacert.pem"):
593+
# Temporary copies of cacert.pem are extracted
594+
# when running from a zipapp
595+
result = True
588596
else:
589597
result = super()._ignore_file(fn)
590598
return result
@@ -696,7 +704,10 @@ def pip(
696704
__tracebackhide__ = True
697705
if self.pip_expect_warning:
698706
kwargs["allow_stderr_warning"] = True
699-
if use_module:
707+
if self.zipapp:
708+
exe = "python"
709+
args = (self.zipapp,) + args
710+
elif use_module:
700711
exe = "python"
701712
args = ("-m", "pip") + args
702713
else:

tests/lib/test_lib.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ def test_correct_pip_version(script: PipTestEnvironment) -> None:
4141
"""
4242
Check we are running proper version of pip in run_pip.
4343
"""
44+
45+
if script.zipapp:
46+
pytest.skip("Test relies on the pip under test being in the filesystem")
47+
4448
# output is like:
4549
# pip PIPVERSION from PIPDIRECTORY (python PYVERSION)
4650
result = script.pip("--version")

0 commit comments

Comments
 (0)