diff --git a/docs/history/hatch.md b/docs/history/hatch.md index 70d506141..796542e35 100644 --- a/docs/history/hatch.md +++ b/docs/history/hatch.md @@ -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*** diff --git a/src/hatch/project/frontend/scripts/standard.py b/src/hatch/project/frontend/scripts/standard.py index 003bf5267..14d305e80 100644 --- a/src/hatch/project/frontend/scripts/standard.py +++ b/src/hatch/project/frontend/scripts/standard.py @@ -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: diff --git a/tests/project/test_frontend.py b/tests/project/test_frontend.py index efbe0779c..67ff32821 100644 --- a/tests/project/test_frontend.py +++ b/tests/project/test_frontend.py @@ -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],