From e05d27aa17ca2b399205bbc8b7560c514c7cc9ce Mon Sep 17 00:00:00 2001 From: Alex Fournier Date: Fri, 18 Sep 2026 22:43:50 +0200 Subject: [PATCH] fix: collect licenses from Python package roots Signed-off-by: Alex Fournier --- scripts/licensing/attributions_lockfile_md.py | 12 +++++++++--- tests/test_attributions.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/scripts/licensing/attributions_lockfile_md.py b/scripts/licensing/attributions_lockfile_md.py index 7f76ff0..d30c511 100644 --- a/scripts/licensing/attributions_lockfile_md.py +++ b/scripts/licensing/attributions_lockfile_md.py @@ -702,14 +702,20 @@ def _declared_wheel_license_texts( def _heuristic_wheel_license_texts( - zf: zipfile.ZipFile, dist_info_dir: str + zf: zipfile.ZipFile, dist_info_dir: str, package_name: str ) -> list[tuple[str, str]]: """Return a fallback wheel license file when metadata does not declare one.""" + normalized_package = _normalize_package_name(package_name) heuristic_paths = _common_license_candidates( [ name for name in zf.namelist() - if name.startswith(f"{dist_info_dir}/licenses/") or name.startswith(f"{dist_info_dir}/") + if name.startswith(f"{dist_info_dir}/licenses/") + or name.startswith(f"{dist_info_dir}/") + or ( + name.count("/") == 1 + and _normalize_package_name(name.split("/", 1)[0]) == normalized_package + ) ] ) heuristic_candidates: list[tuple[str, str, str]] = [] @@ -739,7 +745,7 @@ def _wheel_metadata_from_bytes( ) if not license_texts: # Older wheels often omit License-File entries but still bundle a LICENSE-like file. - license_texts = _heuristic_wheel_license_texts(zf, dist_info_dir) + license_texts = _heuristic_wheel_license_texts(zf, dist_info_dir, package_name) return license_name, license_texts diff --git a/tests/test_attributions.py b/tests/test_attributions.py index 4102a30..fcde8d8 100644 --- a/tests/test_attributions.py +++ b/tests/test_attributions.py @@ -199,6 +199,25 @@ def test_wheel_license_is_rendered_in_relay_format(): assert license_text in text +def test_wheel_license_can_live_at_the_import_package_root(): + data = io.BytesIO() + license_text = "Copyright Example Authors\nPermission granted.\n" + with zipfile.ZipFile(data, "w") as wheel: + wheel.writestr( + "example-1.0.dist-info/METADATA", + "Name: example\nVersion: 1.0\nLicense-Expression: MIT\n", + ) + wheel.writestr("example/LICENSE", license_text) + wheel.writestr("unrelated/LICENSE", "Unrelated dependency license") + + license_name, texts = attribution._wheel_metadata_from_bytes( + data.getvalue(), package_name="example" + ) + + assert license_name == "MIT" + assert texts == [("LICENSE", license_text.rstrip())] + + def test_rust_upstream_fallback_uses_publication_commit(tmp_path, monkeypatch): import json