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 d9e1eb974f..610a5aaee0 100644 --- a/setuptools/command/bdist_wheel.py +++ b/setuptools/command/bdist_wheel.py @@ -590,9 +590,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 0249651267..962da7c34b 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -418,7 +418,10 @@ def _finalize_license_files(self) -> None: patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*'] self.metadata.license_files = list( - unique_everseen(self._expand_patterns(patterns)) + map( + lambda path: path.replace("\\", "/"), + unique_everseen(self._expand_patterns(patterns)), + ) ) @staticmethod @@ -432,7 +435,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..0f2e6ce136 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,9 +354,29 @@ 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 + metadata = wf.read("dummy_dist-1.0.dist-info/METADATA").decode("utf8") + assert "License-File: licenses/DUMMYFILE" in metadata + assert "License-File: LICENSE" in metadata + + +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 + metadata = wf.read("licenses_dist-1.0.dist-info/METADATA").decode("utf8") + assert "License-File: src/vendor/LICENSE" in metadata + assert "License-File: LICENSE" in metadata def test_licenses_disabled(dummy_dist, monkeypatch, tmp_path): diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 121f409057..b26fd2f5b0 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", @@ -438,6 +440,7 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script): for line in ( "Summary: This is a Python package", "License: MIT", + "License-File: LICENSE.txt", "Classifier: Intended Audience :: Developers", "Requires-Dist: appdirs", "Requires-Dist: " + str(Requirement('tomli>=1 ; extra == "all"')), diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 8879ec58ce..9756d7c519 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( @@ -1032,12 +1048,14 @@ def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): license_files = NOTICE* LICENSE* + **/LICENSE """ ), "LICENSE-ABC": "ABC license", "LICENSE-XYZ": "XYZ license", "NOTICE": "included", "IGNORE": "not include", + "vendor": {'LICENSE': "Vendor license"}, }) environment.run_setup_py( @@ -1053,9 +1071,11 @@ def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched # Also assert that order from license_files is keeped + assert len(license_file_lines) == 4 assert "License-File: NOTICE" == license_file_lines[0] assert "License-File: LICENSE-ABC" in license_file_lines[1:] assert "License-File: LICENSE-XYZ" in license_file_lines[1:] + assert "License-File: vendor/LICENSE" in license_file_lines[3] def test_metadata_version(self, tmpdir_cwd, env): """Make sure latest metadata version is used by default."""