diff --git a/newsfragments/4728.feature.rst b/newsfragments/4728.feature.rst new file mode 100644 index 0000000000..61906656c0 --- /dev/null +++ b/newsfragments/4728.feature.rst @@ -0,0 +1 @@ +Store ``License-File``s in ``.dist-info/licenses`` subfolder and added support for recursive globs for ``license_files`` (`PEP 639 `_). -- by :user:`cdce8p` diff --git a/setuptools/command/bdist_wheel.py b/setuptools/command/bdist_wheel.py index 234df2a7c7..5259bf37f0 100644 --- a/setuptools/command/bdist_wheel.py +++ b/setuptools/command/bdist_wheel.py @@ -592,9 +592,11 @@ def adios(p: str) -> None: metadata_path = os.path.join(distinfo_path, "METADATA") shutil.copy(pkginfo_path, metadata_path) + licenses_folder_path = os.path.join(distinfo_path, "licenses") for license_path in self.license_paths: - filename = os.path.basename(license_path) - shutil.copy(license_path, os.path.join(distinfo_path, filename)) + dist_info_license_path = os.path.join(licenses_folder_path, license_path) + os.makedirs(os.path.dirname(dist_info_license_path), exist_ok=True) + shutil.copy(license_path, dist_info_license_path) adios(egginfo_path) diff --git a/setuptools/dist.py b/setuptools/dist.py index 5b3175fb5b..886e1e6c83 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -427,7 +427,7 @@ def _expand_patterns(patterns): return ( path for pattern in patterns - for path in sorted(iglob(pattern)) + for path in sorted(iglob(pattern, recursive=True)) if not path.endswith('~') and os.path.isfile(path) ) diff --git a/setuptools/tests/test_bdist_wheel.py b/setuptools/tests/test_bdist_wheel.py index d51dfbeb6d..b88b2e51fc 100644 --- a/setuptools/tests/test_bdist_wheel.py +++ b/setuptools/tests/test_bdist_wheel.py @@ -172,6 +172,20 @@ ), "README.rst": "UTF-8 描述 説明", }, + "licenses-dist": { + "setup.cfg": cleandoc( + """ + [metadata] + name = licenses-dist + version = 1.0 + license_files = **/LICENSE + """ + ), + "LICENSE": "", + "src": { + "vendor": {"LICENSE": ""}, + }, + }, } @@ -238,6 +252,11 @@ def dummy_dist(tmp_path_factory): return mkexample(tmp_path_factory, "dummy-dist") +@pytest.fixture +def licenses_dist(tmp_path_factory): + return mkexample(tmp_path_factory, "licenses-dist") + + def test_no_scripts(wheel_paths): """Make sure entry point scripts are not generated.""" path = next(path for path in wheel_paths if "complex_dist" in path) @@ -297,7 +316,8 @@ def test_licenses_default(dummy_dist, monkeypatch, tmp_path): bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: license_files = { - "dummy_dist-1.0.dist-info/" + fname for fname in DEFAULT_LICENSE_FILES + "dummy_dist-1.0.dist-info/licenses/" + fname + for fname in DEFAULT_LICENSE_FILES } assert set(wf.namelist()) == DEFAULT_FILES | license_files @@ -311,7 +331,7 @@ def test_licenses_deprecated(dummy_dist, monkeypatch, tmp_path): bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: - license_files = {"dummy_dist-1.0.dist-info/DUMMYFILE"} + license_files = {"dummy_dist-1.0.dist-info/licenses/licenses/DUMMYFILE"} assert set(wf.namelist()) == DEFAULT_FILES | license_files @@ -334,11 +354,25 @@ def test_licenses_override(dummy_dist, monkeypatch, tmp_path, config_file, confi bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: license_files = { - "dummy_dist-1.0.dist-info/" + fname for fname in {"DUMMYFILE", "LICENSE"} + "dummy_dist-1.0.dist-info/licenses/" + fname + for fname in {"licenses/DUMMYFILE", "LICENSE"} } assert set(wf.namelist()) == DEFAULT_FILES | license_files +def test_licenses_preserve_folder_structure(licenses_dist, monkeypatch, tmp_path): + monkeypatch.chdir(licenses_dist) + bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() + print(os.listdir("dist")) + with ZipFile("dist/licenses_dist-1.0-py3-none-any.whl") as wf: + default_files = {name.replace("dummy_", "licenses_") for name in DEFAULT_FILES} + license_files = { + "licenses_dist-1.0.dist-info/licenses/LICENSE", + "licenses_dist-1.0.dist-info/licenses/src/vendor/LICENSE", + } + assert set(wf.namelist()) == default_files | license_files + + def test_licenses_disabled(dummy_dist, monkeypatch, tmp_path): dummy_dist.joinpath("setup.cfg").write_text( "[metadata]\nlicense_files=\n", encoding="utf-8" diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 121f409057..c51e8e345f 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -393,7 +393,9 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script): with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile: wheel_contents = set(zipfile.namelist()) metadata = str(zipfile.read("foo-0.1.dist-info/METADATA"), "utf-8") - license = str(zipfile.read("foo-0.1.dist-info/LICENSE.txt"), "utf-8") + license = str( + zipfile.read("foo-0.1.dist-info/licenses/LICENSE.txt"), "utf-8" + ) epoints = str(zipfile.read("foo-0.1.dist-info/entry_points.txt"), "utf-8") assert sdist_contents - {"foo-0.1/setup.py"} == { @@ -426,7 +428,7 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script): "foo/cli.py", "foo/data.txt", # include_package_data defaults to True "foo/py.typed", # include type information by default - "foo-0.1.dist-info/LICENSE.txt", + "foo-0.1.dist-info/licenses/LICENSE.txt", "foo-0.1.dist-info/METADATA", "foo-0.1.dist-info/WHEEL", "foo-0.1.dist-info/entry_points.txt", diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index a68ecaba4c..8417493c68 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -815,6 +815,22 @@ def test_setup_cfg_license_file(self, tmpdir_cwd, env, files, license_in_sources [], id="files_only_added_once", ), + pytest.param( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = **/LICENSE + """ + ), + 'LICENSE': "ABC license", + 'LICENSE-OTHER': "Don't include", + 'vendor': {'LICENSE': "Vendor license"}, + }, + ['LICENSE', 'vendor/LICENSE'], + ['LICENSE-OTHER'], + id="recursive_glob", + ), ], ) def test_setup_cfg_license_files(