Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
wip
  • Loading branch information
ichard26 committed Jul 30, 2025
commit 10ebd8febab3b6782c269e5f1a9b511878eadb5a
18 changes: 13 additions & 5 deletions src/pip/_internal/build_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ def __init__(
from pip._internal.operations.prepare import RequirementPreparer

self.finder = finder
self._level = 0

# TODO: I don't think this is right
build_dir = TempDirectory(kind="build-env-install", globally_managed=True)
Expand Down Expand Up @@ -320,7 +321,7 @@ def install(
"""Install entrypoint. Manages output capturing and error handling."""
capture_ctx: AbstractContextManager[StringIO]
spinner: AbstractContextManager[None]
should_capture = not logger.isEnabledFor(VERBOSE)
should_capture = not logger.isEnabledFor(VERBOSE) and self._level == 0
if should_capture:
# Hide the logs from the installation of build dependencies.
# They will be shown only if an error occurs.
Expand All @@ -333,21 +334,27 @@ def install(
logger.info("Installing %s ...", kind)

try:
self._level += 1
with spinner, capture_ctx as stream:
self._install_impl(requirements, prefix)
except Exception as exc:
log_lines = textwrap.dedent(stream.getvalue()).splitlines()
if isinstance(exc, DiagnosticPipError):
# Format similar to a nested subprocess error, where the
# causing error is shown first, followed by the build error.
for l in log_lines:
logger.info(l)
log_lines = []
logger.error("%s", exc, extra={"rich": True})
logger.info("")
elif not should_capture:
logger.error("%s", exc)

raise BuildDependencyInstallError(
for_req,
requirements,
cause=exc,
log_lines=textwrap.dedent(stream.getvalue()).splitlines(),
for_req, requirements, cause=exc, log_lines=log_lines
)
finally:
self._level -= 1

def _install_impl(self, requirements: Iterable[str], prefix: _Prefix) -> None:
"""Core build dependency install logic."""
Expand Down Expand Up @@ -378,6 +385,7 @@ def _install_impl(self, requirements: Iterable[str], prefix: _Prefix) -> None:
build_options=[],
global_options=[],
)
# build_failures = ireqs
if build_failures:
raise InstallWheelBuildError(build_failures)

Expand Down
2 changes: 2 additions & 0 deletions src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ def get_requirements(
requirements.append(req_to_add)

for req in args:
if not req.strip():
continue
req_to_add = install_req_from_line(
req,
comes_from=None,
Expand Down
16 changes: 6 additions & 10 deletions src/pip/_internal/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,18 +911,11 @@ def __init__(
cause: Exception,
log_lines: list[str],
) -> None:
hint = None

if not log_lines:
# No logs are available, they must have been printed earlier.
context = Text(f"ERROR: {cause}")
hint = "Look above for more details."
context = Text("See above for more details.")
else:
if isinstance(cause, DiagnosticPipError):
hint = "Look above for the original error that caused this failure."
else:
log_lines.append(f"ERROR: {cause}")

log_lines.append(f"ERROR: {cause}")
context = Text.assemble(
f"Installing {' '.join(build_reqs)}\n",
(f"[{len(log_lines)} lines of output]\n", "red"),
Expand All @@ -933,4 +926,7 @@ def __init__(
message = Text("Cannot install build dependencies", "green")
if req:
message += Text(f" for {req}")
super().__init__(message=message, context=context, hint_stmt=hint)
note = "This is likely not a problem with pip."
super().__init__(
message=message, context=context, hint_stmt=None, note_stmt=note
)
2 changes: 2 additions & 0 deletions src/pip/_internal/network/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ def is_incomplete(self) -> bool:
return bool(self.size is not None and self.bytes_received < self.size)

def write_chunk(self, data: bytes) -> None:
# import random
# if random.random() > 0.5:
self.bytes_received += len(data)
self.output_file.write(data)

Expand Down
6 changes: 5 additions & 1 deletion src/pip/_internal/utils/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,14 @@ def capture_logging() -> Generator[StringIO, None, None]:
# restored on context exit.
handlers[handler] = handler.console

fake_stream = StreamWrapper.from_stream(sys.stdout)
if not handlers:
yield fake_stream
return

# HACK: grab no_color attribute from a random handler console since
# it's a global option anyway.
no_color = next(iter(handlers.values())).no_color
fake_stream = StreamWrapper.from_stream(sys.stdout)
fake_console = PipConsole(file=fake_stream, no_color=no_color, soft_wrap=True)
try:
for handler in handlers:
Expand Down
97 changes: 77 additions & 20 deletions tests/functional/test_build_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@

import os
import sys
from contextlib import contextmanager
from textwrap import dedent
from typing import Generator, Literal

import pytest

from pip._internal.build_env import (
BuildEnvironment,
BuildEnvironmentInstaller,
InprocessBuildEnvironmentInstaller,
SubprocessBuildEnvironmentInstaller,
_get_system_sitepackages,
)
from pip._internal.cache import WheelCache
from pip._internal.index.package_finder import PackageFinder
from pip._internal.operations.build.build_tracker import get_build_tracker

from tests.lib import (
PipTestEnvironment,
Expand All @@ -19,15 +26,36 @@
make_test_finder,
)

InstallMethod = Literal["subprocess", "inprocess"]
with_both_installers = pytest.mark.parametrize(
"install_method", ["subprocess", "inprocess"]
)


def indent(text: str, prefix: str) -> str:
return "\n".join((prefix if line else "") + line for line in text.split("\n"))


@contextmanager
def make_test_build_env_installer(
method: InstallMethod, finder: PackageFinder
) -> Generator[BuildEnvironmentInstaller]:
if method == "subprocess":
yield SubprocessBuildEnvironmentInstaller(finder)
else:
with get_build_tracker() as tracker:
yield InprocessBuildEnvironmentInstaller(
finder=finder,
build_tracker=tracker,
wheel_cache=WheelCache(None),
)


def run_with_build_env(
script: PipTestEnvironment,
setup_script_contents: str,
test_script_contents: str | None = None,
install_method: InstallMethod = "subprocess",
) -> TestPipResult:
build_env_script = script.scratch_path / "build_env.py"
scratch_path = str(script.scratch_path)
Expand All @@ -39,14 +67,17 @@ def run_with_build_env(

from pip._internal.build_env import (
BuildEnvironment,
InprocessBuildEnvironmentInstaller,
SubprocessBuildEnvironmentInstaller,
)
from pip._internal.cache import WheelCache
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.search_scope import SearchScope
from pip._internal.models.selection_prefs import (
SelectionPreferences
)
from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.network.session import PipSession
from pip._internal.utils.temp_dir import global_tempdir_manager

Expand All @@ -62,10 +93,16 @@ def run_with_build_env(
selection_prefs=selection_prefs,
)

with global_tempdir_manager():
build_env = BuildEnvironment(
SubprocessBuildEnvironmentInstaller(finder)
)
with global_tempdir_manager(), get_build_tracker() as tracker:
if "{install_method}" == "subprocess":
installer = SubprocessBuildEnvironmentInstaller(finder)
else:
installer = InprocessBuildEnvironmentInstaller(
finder=finder,
build_tracker=tracker,
wheel_cache=WheelCache(None),
)
build_env = BuildEnvironment(installer)
"""
)
+ indent(dedent(setup_script_contents), " ")
Expand All @@ -88,28 +125,40 @@ def run_with_build_env(
return script.run(*args)


def test_build_env_allow_empty_requirements_install() -> None:
@with_both_installers
def test_build_env_allow_empty_requirements_install(
install_method: InstallMethod,
) -> None:
finder = make_test_finder()
build_env = BuildEnvironment(SubprocessBuildEnvironmentInstaller(finder))
for prefix in ("normal", "overlay"):
build_env.install_requirements([], prefix, kind="Installing build dependencies")
with make_test_build_env_installer(install_method, finder) as installer:
build_env = BuildEnvironment(installer)
for prefix in ("normal", "overlay"):
build_env.install_requirements(
[], prefix, kind="Installing build dependencies"
)


def test_build_env_allow_only_one_install(script: PipTestEnvironment) -> None:
@with_both_installers
def test_build_env_allow_only_one_install(
script: PipTestEnvironment, install_method: InstallMethod
) -> None:
create_basic_wheel_for_package(script, "foo", "1.0")
create_basic_wheel_for_package(script, "bar", "1.0")
finder = make_test_finder(find_links=[os.fspath(script.scratch_path)])
build_env = BuildEnvironment(SubprocessBuildEnvironmentInstaller(finder))
for prefix in ("normal", "overlay"):
build_env.install_requirements(
["foo"], prefix, kind=f"installing foo in {prefix}"
)
with pytest.raises(AssertionError):
with make_test_build_env_installer(install_method, finder) as installer:
build_env = BuildEnvironment(installer)
for prefix in ("normal", "overlay"):
build_env.install_requirements(
["bar"], prefix, kind=f"installing bar in {prefix}"
["foo"], prefix, kind=f"installing foo in {prefix}"
)
with pytest.raises(AssertionError):
build_env.install_requirements([], prefix, kind=f"installing in {prefix}")
with pytest.raises(AssertionError):
build_env.install_requirements(
["bar"], prefix, kind=f"installing bar in {prefix}"
)
with pytest.raises(AssertionError):
build_env.install_requirements(
[], prefix, kind=f"installing in {prefix}"
)


def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
Expand Down Expand Up @@ -191,7 +240,10 @@ def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
)


def test_build_env_overlay_prefix_has_priority(script: PipTestEnvironment) -> None:
@with_both_installers
def test_build_env_overlay_prefix_has_priority(
script: PipTestEnvironment, install_method: InstallMethod
) -> None:
create_basic_wheel_for_package(script, "pkg", "2.0")
create_basic_wheel_for_package(script, "pkg", "4.3")
result = run_with_build_env(
Expand All @@ -205,6 +257,7 @@ def test_build_env_overlay_prefix_has_priority(script: PipTestEnvironment) -> No
"""
print(__import__('pkg').__version__)
""",
install_method=install_method,
)
assert result.stdout.strip() == "2.0", str(result)

Expand Down Expand Up @@ -234,8 +287,11 @@ def test_build_env_overlay_prefix_has_priority(script: PipTestEnvironment) -> No
"""


@with_both_installers
@pytest.mark.usefixtures("enable_user_site")
def test_build_env_isolation(script: PipTestEnvironment) -> None:
def test_build_env_isolation(
script: PipTestEnvironment, install_method: InstallMethod
) -> None:
# Create dummy `pkg` wheel.
pkg_whl = create_basic_wheel_for_package(script, "pkg", "1.0")

Expand Down Expand Up @@ -281,4 +337,5 @@ def test_build_env_isolation(script: PipTestEnvironment) -> None:
assert system_path not in normalized_path, \
f"{{system_path}} found in {{normalized_path}}"
""",
install_method=install_method,
)
Loading