Skip to content

Commit 0d6fada

Browse files
committed
Merge branch 'main' into python_option
2 parents 4dc35b7 + f47a204 commit 0d6fada

File tree

17 files changed

+151
-17
lines changed

17 files changed

+151
-17
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

docs/html/cli/pip_download.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ match the constraint of the current interpreter (but not your target one), it
4343
is recommended to specify all of these options if you are specifying one of
4444
them. Generic dependencies (e.g. universal wheels, or dependencies with no
4545
platform, abi, or implementation constraints) will still match an over-
46-
constrained download requirement.
46+
constrained download requirement. If some of your dependencies are not
47+
available as binaries, you can build them manually for your target platform
48+
and let pip download know where to find them using ``--find-links``.
4749

4850

4951

docs/html/user_guide.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
User Guide
33
==========
44

5+
.. Hello there!
6+
7+
If you're thinking of adding content to this page... please take a moment
8+
to consider if this content can live on its own, within a topic guide or a
9+
reference page.
10+
11+
There is active effort being put toward *reducing* the amount of content on
12+
this specific page (https://github.com/pypa/pip/issues/9475) and moving it
13+
into more focused single-page documents that cover that specific topic.
514
615
Running pip
716
===========

news/10716.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use ``shell=True`` for opening the editor with ``pip config edit``.

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.

news/11314.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Avoid ``AttributeError`` when removing the setuptools-provided ``_distutils_hack`` and it is missing its implementation.

news/11319.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix import error when reinstalling pip in user site.

src/pip/__pip-runner__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ def check_python_version() -> None:
4545
)
4646

4747

48-
# TODO https://github.com/pypa/pip/issues/11294
4948
sys.meta_path.insert(0, PipImportRedirectingFinder())
5049

5150
assert __name__ == "__main__", "Cannot run __pip-runner__.py as a non-main module"

src/pip/_internal/commands/configuration.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,15 @@ def open_in_editor(self, options: Values, args: List[str]) -> None:
228228
fname = self.configuration.get_file_to_edit()
229229
if fname is None:
230230
raise PipError("Could not determine appropriate file.")
231+
elif '"' in fname:
232+
# This shouldn't happen, unless we see a username like that.
233+
# If that happens, we'd appreciate a pull request fixing this.
234+
raise PipError(
235+
f'Can not open an editor for a file name containing "\n{fname}'
236+
)
231237

232238
try:
233-
subprocess.check_call([editor, fname])
239+
subprocess.check_call(f'{editor} "{fname}"', shell=True)
234240
except FileNotFoundError as e:
235241
if not e.filename:
236242
e.filename = editor

src/pip/_internal/locations/__init__.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ def _should_use_sysconfig() -> bool:
6060

6161
_USE_SYSCONFIG = _should_use_sysconfig()
6262

63+
if not _USE_SYSCONFIG:
64+
# Import distutils lazily to avoid deprecation warnings,
65+
# but import it soon enough that it is in memory and available during
66+
# a pip reinstall.
67+
from . import _distutils
68+
6369
# Be noisy about incompatibilities if this platforms "should" be using
6470
# sysconfig, but is explicitly opting out and using distutils instead.
6571
if _USE_SYSCONFIG_DEFAULT and not _USE_SYSCONFIG:
@@ -241,8 +247,6 @@ def get_scheme(
241247
if _USE_SYSCONFIG:
242248
return new
243249

244-
from . import _distutils
245-
246250
old = _distutils.get_scheme(
247251
dist_name,
248252
user=user,
@@ -407,8 +411,6 @@ def get_bin_prefix() -> str:
407411
if _USE_SYSCONFIG:
408412
return new
409413

410-
from . import _distutils
411-
412414
old = _distutils.get_bin_prefix()
413415
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"):
414416
_log_context()
@@ -442,8 +444,6 @@ def get_purelib() -> str:
442444
if _USE_SYSCONFIG:
443445
return new
444446

445-
from . import _distutils
446-
447447
old = _distutils.get_purelib()
448448
if _looks_like_deb_system_dist_packages(old):
449449
return old
@@ -488,8 +488,6 @@ def get_prefixed_libs(prefix: str) -> List[str]:
488488
if _USE_SYSCONFIG:
489489
return _deduplicated(new_pure, new_plat)
490490

491-
from . import _distutils
492-
493491
old_pure, old_plat = _distutils.get_prefixed_libs(prefix)
494492
old_lib_paths = _deduplicated(old_pure, old_plat)
495493

src/pip/_internal/locations/_distutils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# rationale for why this is done within pip.
1212
try:
1313
__import__("_distutils_hack").remove_shim()
14-
except ImportError:
14+
except (ImportError, AttributeError):
1515
pass
1616

1717
import logging

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)