fix(packaging): Format `METADATA` correctly if given empty `requires_file` (#2771)
An empty `requires_file` used to be okay, but at some point regressed to
leaving an empty line (due to the `metadata.replace(...)`) in the
`METADATA` file - rendering the wheel uninstallable.
This PR initially attempted to solve that by introducing a new list that
processed `METADATA` lines go into, rather than relying on repeated
string replacement. But it seems like the repeated string replace
actually did more than simply process one line at a time, so I reverted
to a single substitution at the end.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f86851..e7f9fe3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -96,6 +96,7 @@
* (toolchains) Run the check on the Python interpreter in isolated mode, to ensure it's not affected by userland environment variables, such as `PYTHONPATH`.
* (toolchains) Ensure temporary `.pyc` and `.pyo` files are also excluded from the interpreters repository files.
* (pypi) Run interpreter version call in isolated mode, to ensure it's not affected by userland environment variables, such as `PYTHONPATH`.
+* (packaging) An empty `requires_file` is treated as if it were omitted, resulting in a valid `METADATA` file.
{#v0-0-0-added}
### Added
diff --git a/examples/wheel/BUILD.bazel b/examples/wheel/BUILD.bazel
index d9ba800..b434e67 100644
--- a/examples/wheel/BUILD.bazel
+++ b/examples/wheel/BUILD.bazel
@@ -295,6 +295,12 @@
)
write_file(
+ name = "empty_requires_file",
+ out = "empty_requires.txt",
+ content = [""],
+)
+
+write_file(
name = "extra_requires_file",
out = "extra_requires.txt",
content = """\
@@ -324,6 +330,15 @@
deps = [":example_pkg"],
)
+py_wheel(
+ name = "empty_requires_files",
+ distribution = "empty_requires_files",
+ python_tag = "py3",
+ requires_file = ":empty_requires.txt",
+ version = "0.0.1",
+ deps = [":example_pkg"],
+)
+
# Package just a specific py_libraries, without their dependencies
py_wheel(
name = "minimal_data_files",
@@ -367,6 +382,7 @@
":custom_package_root_multi_prefix",
":custom_package_root_multi_prefix_reverse_order",
":customized",
+ ":empty_requires_files",
":extra_requires",
":filename_escaping",
":minimal_data_files",
diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py
index a3d6034..9ec1503 100644
--- a/examples/wheel/wheel_test.py
+++ b/examples/wheel/wheel_test.py
@@ -483,7 +483,6 @@
if line.startswith(b"Requires-Dist:"):
requires.append(line.decode("utf-8").strip())
- print(requires)
self.assertEqual(
[
"Requires-Dist: tomli>=2.0.0",
@@ -495,6 +494,29 @@
requires,
)
+ def test_empty_requires_file(self):
+ filename = self._get_path("empty_requires_files-0.0.1-py3-none-any.whl")
+
+ with zipfile.ZipFile(filename) as zf:
+ self.assertAllEntriesHasReproducibleMetadata(zf)
+ metadata_file = None
+ for f in zf.namelist():
+ if os.path.basename(f) == "METADATA":
+ metadata_file = f
+ self.assertIsNotNone(metadata_file)
+
+ metadata = zf.read(metadata_file).decode("utf-8")
+ metadata_lines = metadata.splitlines()
+
+ requires = []
+ for i, line in enumerate(metadata_lines):
+ if line.startswith("Name:"):
+ self.assertTrue(metadata_lines[i + 1].startswith("Version:"))
+ if line.startswith("Requires-Dist:"):
+ requires.append(line.strip())
+
+ self.assertEqual([], requires)
+
def test_minimal_data_files(self):
filename = self._get_path("minimal_data_files-0.0.1-py3-none-any.whl")
diff --git a/python/packaging.bzl b/python/packaging.bzl
index 629af2d..b190635 100644
--- a/python/packaging.bzl
+++ b/python/packaging.bzl
@@ -101,6 +101,11 @@
Currently only pure-python wheels are supported.
+ :::{versionchanged} VERSION_NEXT_FEATURE
+ From now on, an empty `requires_file` is treated as if it were omitted, resulting in a valid
+ `METADATA` file.
+ :::
+
Examples:
```python
diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py
index 23b18ec..908b3fe 100644
--- a/tools/wheelmaker.py
+++ b/tools/wheelmaker.py
@@ -599,7 +599,12 @@
reqs.append(get_new_requirement_line(reqs_text, extra))
- metadata = metadata.replace(meta_line, "\n".join(reqs))
+ if reqs:
+ metadata = metadata.replace(meta_line, "\n".join(reqs))
+ # File is empty
+ # So replace the meta_line entirely, including removing newline chars
+ else:
+ metadata = re.sub(re.escape(meta_line) + r"(?:\r?\n)?", "", metadata, count=1)
maker.add_metadata(
metadata=metadata,