Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions docs/history/hatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## Unreleased

***Fixed:***

- Prevent output from non-Hatchling build backends from polluting stdout when reading project metadata.

## [1.17.1](https://github.com/pypa/hatch/releases/tag/hatch-v1.17.1) - 2026-07-08 ## {: #hatch-v1.17.1 }

***Fixed***
Expand Down
1 change: 1 addition & 0 deletions src/hatch/project/frontend/scripts/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def main() -> int:
[sys.executable, script_path, hook, str(control_dir)],
cwd=project_root,
env=env_vars,
stdout=sys.stderr,
check=False,
)
if process.returncode:
Expand Down
61 changes: 61 additions & 0 deletions tests/project/test_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,67 @@ def test_wheel(self, temp_dir, temp_dir_data, platform, global_application, back
"description": "text",
}

def test_backend_stdout_is_forwarded_to_stderr(self, temp_dir, temp_dir_data, platform, global_application):
# Non-Hatchling backends (notably setuptools/setuptools-scm) may print build
# progress to stdout while preparing metadata. Keep Hatch command stdout clean
# by forwarding that stream to stderr (see pypa/hatch#2069).
project_dir = temp_dir / "project"
project_dir.mkdir()
(project_dir / "pyproject.toml").write_text(
"""\
[build-system]
requires = []
build-backend = "noisy_backend"
backend-path = ["."]

[project]
name = "foo"
version = "9000.42"
"""
)
(project_dir / "noisy_backend.py").write_text(
"""\
import os
import sys


def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
print("backend stdout")
print("backend stderr", file=sys.stderr)
dist_info = os.path.join(metadata_directory, "foo-9000.42.dist-info")
os.mkdir(dist_info)
with open(os.path.join(dist_info, "METADATA"), "w", encoding="utf-8") as f:
f.write("Metadata-Version: 2.4\\nName: foo\\nVersion: 9000.42\\n")
return "foo-9000.42.dist-info"
"""
)

project = Project(project_dir)
project.build_env = MockEnvironment(
temp_dir,
project.metadata,
"default",
project.config.envs["default"],
{},
temp_dir_data,
temp_dir_data,
platform,
0,
global_application,
)

output_dir = temp_dir / "output"
output_dir.mkdir()
script = project.build_frontend.scripts.prepare_metadata(
output_dir=str(output_dir), project_root=str(project_dir)
)
process = platform.run_command([sys.executable, "-c", script], capture_output=True, text=True)

assert process.returncode == 0
assert process.stdout == ""
assert "backend stdout" in process.stderr
assert "backend stderr" in process.stderr

@pytest.mark.parametrize(
("backend_pkg", "backend_api"),
[pytest.param(backend_pkg, backend_api, id=backend_pkg) for backend_pkg, backend_api in BACKENDS],
Expand Down