CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
test_bdist_wheel.py709 linesDownload Raw Back to tests
1from __future__ import annotations2 3import builtins4import importlib5import os.path6import platform7import shutil8import stat9import struct10import sys11import sysconfig12from contextlib import suppress13from inspect import cleandoc14from zipfile import ZipFile15 16import jaraco.path17import pytest18from packaging import tags19 20import setuptools21from setuptools.command.bdist_wheel import bdist_wheel, get_abi_tag22from setuptools.dist import Distribution23from setuptools.warnings import SetuptoolsDeprecationWarning24 25from distutils.core import run_setup26 27DEFAULT_FILES = {28    "dummy_dist-1.0.dist-info/top_level.txt",29    "dummy_dist-1.0.dist-info/METADATA",30    "dummy_dist-1.0.dist-info/WHEEL",31    "dummy_dist-1.0.dist-info/RECORD",32}33DEFAULT_LICENSE_FILES = {34    "LICENSE",35    "LICENSE.txt",36    "LICENCE",37    "LICENCE.txt",38    "COPYING",39    "COPYING.md",40    "NOTICE",41    "NOTICE.rst",42    "AUTHORS",43    "AUTHORS.txt",44}45OTHER_IGNORED_FILES = {46    "LICENSE~",47    "AUTHORS~",48}49SETUPPY_EXAMPLE = """\50from setuptools import setup51 52setup(53    name='dummy_dist',54    version='1.0',55)56"""57 58 59EXAMPLES = {60    "dummy-dist": {61        "setup.py": SETUPPY_EXAMPLE,62        "licenses_dir": {"DUMMYFILE": ""},63        **dict.fromkeys(DEFAULT_LICENSE_FILES | OTHER_IGNORED_FILES, ""),64    },65    "simple-dist": {66        "setup.py": cleandoc(67            """68            from setuptools import setup69 70            setup(71                name="simple.dist",72                version="0.1",73                description="A testing distribution \N{SNOWMAN}",74                extras_require={"voting": ["beaglevote"]},75            )76            """77        ),78        "simpledist": "",79    },80    "complex-dist": {81        "setup.py": cleandoc(82            """83            from setuptools import setup84 85            setup(86                name="complex-dist",87                version="0.1",88                description="Another testing distribution \N{SNOWMAN}",89                long_description="Another testing distribution \N{SNOWMAN}",90                author="Illustrious Author",91                author_email="illustrious@example.org",92                url="http://example.org/exemplary",93                packages=["complexdist"],94                setup_requires=["setuptools"],95                install_requires=["quux", "splort"],96                extras_require={"simple": ["simple.dist"]},97                entry_points={98                    "console_scripts": [99                        "complex-dist=complexdist:main",100                        "complex-dist2=complexdist:main",101                    ],102                },103            )104            """105        ),106        "complexdist": {"__init__.py": "def main(): return"},107    },108    "headers-dist": {109        "setup.py": cleandoc(110            """111            from setuptools import setup112 113            setup(114                name="headers.dist",115                version="0.1",116                description="A distribution with headers",117                headers=["header.h"],118            )119            """120        ),121        "headersdist.py": "",122        "header.h": "",123    },124    "commasinfilenames-dist": {125        "setup.py": cleandoc(126            """127            from setuptools import setup128 129            setup(130                name="testrepo",131                version="0.1",132                packages=["mypackage"],133                description="A test package with commas in file names",134                include_package_data=True,135                package_data={"mypackage.data": ["*"]},136            )137            """138        ),139        "mypackage": {140            "__init__.py": "",141            "data": {"__init__.py": "", "1,2,3.txt": ""},142        },143        "testrepo-0.1.0": {144            "mypackage": {"__init__.py": ""},145        },146    },147    "unicode-dist": {148        "setup.py": cleandoc(149            """150            from setuptools import setup151 152            setup(153                name="unicode.dist",154                version="0.1",155                description="A testing distribution \N{SNOWMAN}",156                packages=["unicodedist"],157                zip_safe=True,158            )159            """160        ),161        "unicodedist": {"__init__.py": "", "åäö_日本語.py": ""},162    },163    "utf8-metadata-dist": {164        "setup.cfg": cleandoc(165            """166            [metadata]167            name = utf8-metadata-dist168            version = 42169            author_email = "John X. Ãørçeč" <john@utf8.org>, Γαμα קּ 東 <gama@utf8.org>170            long_description = file: README.rst171            """172        ),173        "README.rst": "UTF-8 描述 説明",174    },175    "licenses-dist": {176        "setup.cfg": cleandoc(177            """178            [metadata]179            name = licenses-dist180            version = 1.0181            license_files = **/LICENSE182            """183        ),184        "LICENSE": "",185        "src": {186            "vendor": {"LICENSE": ""},187        },188    },189}190 191 192if sys.platform != "win32":193    # ABI3 extensions don't really work on Windows194    EXAMPLES["abi3extension-dist"] = {195        "setup.py": cleandoc(196            """197            from setuptools import Extension, setup198 199            setup(200                name="extension.dist",201                version="0.1",202                description="A testing distribution \N{SNOWMAN}",203                ext_modules=[204                    Extension(205                        name="extension", sources=["extension.c"], py_limited_api=True206                    )207                ],208            )209            """210        ),211        "setup.cfg": "[bdist_wheel]\npy_limited_api=cp32",212        "extension.c": "#define Py_LIMITED_API 0x03020000\n#include <Python.h>",213    }214 215 216def bdist_wheel_cmd(**kwargs):217    """Run command in the same process so that it is easier to collect coverage"""218    dist_obj = (219        run_setup("setup.py", stop_after="init")220        if os.path.exists("setup.py")221        else Distribution({"script_name": "%%build_meta%%"})222    )223    dist_obj.parse_config_files()224    cmd = bdist_wheel(dist_obj)225    for attr, value in kwargs.items():226        setattr(cmd, attr, value)227    cmd.finalize_options()228    return cmd229 230 231def mkexample(tmp_path_factory, name):232    basedir = tmp_path_factory.mktemp(name)233    jaraco.path.build(EXAMPLES[name], prefix=str(basedir))234    return basedir235 236 237@pytest.fixture(scope="session")238def wheel_paths(tmp_path_factory):239    build_base = tmp_path_factory.mktemp("build")240    dist_dir = tmp_path_factory.mktemp("dist")241    for name in EXAMPLES:242        example_dir = mkexample(tmp_path_factory, name)243        build_dir = build_base / name244        with jaraco.path.DirectoryStack().context(example_dir):245            bdist_wheel_cmd(bdist_dir=str(build_dir), dist_dir=str(dist_dir)).run()246 247    return sorted(str(fname) for fname in dist_dir.glob("*.whl"))248 249 250@pytest.fixture251def dummy_dist(tmp_path_factory):252    return mkexample(tmp_path_factory, "dummy-dist")253 254 255@pytest.fixture256def licenses_dist(tmp_path_factory):257    return mkexample(tmp_path_factory, "licenses-dist")258 259 260def test_no_scripts(wheel_paths):261    """Make sure entry point scripts are not generated."""262    path = next(path for path in wheel_paths if "complex_dist" in path)263    for entry in ZipFile(path).infolist():264        assert ".data/scripts/" not in entry.filename265 266 267def test_unicode_record(wheel_paths):268    path = next(path for path in wheel_paths if "unicode_dist" in path)269    with ZipFile(path) as zf:270        record = zf.read("unicode_dist-0.1.dist-info/RECORD")271 272    assert "åäö_日本語.py".encode() in record273 274 275UTF8_PKG_INFO = """\276Metadata-Version: 2.1277Name: helloworld278Version: 42279Author-email: "John X. Ãørçeč" <john@utf8.org>, Γαμα קּ 東 <gama@utf8.org>280 281 282UTF-8 描述 説明283"""284 285 286def test_preserve_unicode_metadata(monkeypatch, tmp_path):287    monkeypatch.chdir(tmp_path)288    egginfo = tmp_path / "dummy_dist.egg-info"289    distinfo = tmp_path / "dummy_dist.dist-info"290 291    egginfo.mkdir()292    (egginfo / "PKG-INFO").write_text(UTF8_PKG_INFO, encoding="utf-8")293    (egginfo / "dependency_links.txt").touch()294 295    class simpler_bdist_wheel(bdist_wheel):296        """Avoid messing with setuptools/distutils internals"""297 298        def __init__(self):299            pass300 301        @property302        def license_paths(self):303            return []304 305    cmd_obj = simpler_bdist_wheel()306    cmd_obj.egg2dist(egginfo, distinfo)307 308    metadata = (distinfo / "METADATA").read_text(encoding="utf-8")309    assert 'Author-email: "John X. Ãørçeč"' in metadata310    assert "Γαμα קּ 東 " in metadata311    assert "UTF-8 描述 説明" in metadata312 313 314def test_licenses_default(dummy_dist, monkeypatch, tmp_path):315    monkeypatch.chdir(dummy_dist)316    bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()317    with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:318        license_files = {319            "dummy_dist-1.0.dist-info/licenses/" + fname320            for fname in DEFAULT_LICENSE_FILES321        }322        assert set(wf.namelist()) == DEFAULT_FILES | license_files323 324 325def test_licenses_deprecated(dummy_dist, monkeypatch, tmp_path):326    dummy_dist.joinpath("setup.cfg").write_text(327        "[metadata]\nlicense_file=licenses_dir/DUMMYFILE", encoding="utf-8"328    )329    monkeypatch.chdir(dummy_dist)330 331    bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()332 333    with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:334        license_files = {"dummy_dist-1.0.dist-info/licenses/licenses_dir/DUMMYFILE"}335        assert set(wf.namelist()) == DEFAULT_FILES | license_files336 337 338@pytest.mark.parametrize(339    ("config_file", "config"),340    [341        ("setup.cfg", "[metadata]\nlicense_files=licenses_dir/*\n  LICENSE"),342        ("setup.cfg", "[metadata]\nlicense_files=licenses_dir/*, LICENSE"),343        (344            "setup.py",345            SETUPPY_EXAMPLE.replace(346                ")", "  license_files=['licenses_dir/DUMMYFILE', 'LICENSE'])"347            ),348        ),349    ],350)351def test_licenses_override(dummy_dist, monkeypatch, tmp_path, config_file, config):352    dummy_dist.joinpath(config_file).write_text(config, encoding="utf-8")353    monkeypatch.chdir(dummy_dist)354    bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()355    with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:356        license_files = {357            "dummy_dist-1.0.dist-info/licenses/" + fname358            for fname in {"licenses_dir/DUMMYFILE", "LICENSE"}359        }360        assert set(wf.namelist()) == DEFAULT_FILES | license_files361        metadata = wf.read("dummy_dist-1.0.dist-info/METADATA").decode("utf8")362        assert "License-File: licenses_dir/DUMMYFILE" in metadata363        assert "License-File: LICENSE" in metadata364 365 366def test_licenses_preserve_folder_structure(licenses_dist, monkeypatch, tmp_path):367    monkeypatch.chdir(licenses_dist)368    bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()369    print(os.listdir("dist"))370    with ZipFile("dist/licenses_dist-1.0-py3-none-any.whl") as wf:371        default_files = {name.replace("dummy_", "licenses_") for name in DEFAULT_FILES}372        license_files = {373            "licenses_dist-1.0.dist-info/licenses/LICENSE",374            "licenses_dist-1.0.dist-info/licenses/src/vendor/LICENSE",375        }376        assert set(wf.namelist()) == default_files | license_files377        metadata = wf.read("licenses_dist-1.0.dist-info/METADATA").decode("utf8")378        assert "License-File: src/vendor/LICENSE" in metadata379        assert "License-File: LICENSE" in metadata380 381 382def test_licenses_disabled(dummy_dist, monkeypatch, tmp_path):383    dummy_dist.joinpath("setup.cfg").write_text(384        "[metadata]\nlicense_files=\n", encoding="utf-8"385    )386    monkeypatch.chdir(dummy_dist)387    bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()388    with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:389        assert set(wf.namelist()) == DEFAULT_FILES390 391 392def test_build_number(dummy_dist, monkeypatch, tmp_path):393    monkeypatch.chdir(dummy_dist)394    bdist_wheel_cmd(bdist_dir=str(tmp_path), build_number="2").run()395    with ZipFile("dist/dummy_dist-1.0-2-py3-none-any.whl") as wf:396        filenames = set(wf.namelist())397        assert "dummy_dist-1.0.dist-info/RECORD" in filenames398        assert "dummy_dist-1.0.dist-info/METADATA" in filenames399 400 401def test_universal_deprecated(dummy_dist, monkeypatch, tmp_path):402    monkeypatch.chdir(dummy_dist)403    with pytest.warns(SetuptoolsDeprecationWarning, match=".*universal is deprecated"):404        bdist_wheel_cmd(bdist_dir=str(tmp_path), universal=True).run()405 406    # For now we still respect the option407    assert os.path.exists("dist/dummy_dist-1.0-py2.py3-none-any.whl")408 409 410EXTENSION_EXAMPLE = """\411#include <Python.h>412 413static PyMethodDef methods[] = {414  { NULL, NULL, 0, NULL }415};416 417static struct PyModuleDef module_def = {418  PyModuleDef_HEAD_INIT,419  "extension",420  "Dummy extension module",421  -1,422  methods423};424 425PyMODINIT_FUNC PyInit_extension(void) {426  return PyModule_Create(&module_def);427}428"""429EXTENSION_SETUPPY = """\430from __future__ import annotations431 432from setuptools import Extension, setup433 434setup(435    name="extension.dist",436    version="0.1",437    description="A testing distribution \N{SNOWMAN}",438    ext_modules=[Extension(name="extension", sources=["extension.c"])],439)440"""441 442 443@pytest.mark.filterwarnings(444    "once:Config variable '.*' is unset.*, Python ABI tag may be incorrect"445)446def test_limited_abi(monkeypatch, tmp_path, tmp_path_factory):447    """Test that building a binary wheel with the limited ABI works."""448    source_dir = tmp_path_factory.mktemp("extension_dist")449    (source_dir / "setup.py").write_text(EXTENSION_SETUPPY, encoding="utf-8")450    (source_dir / "extension.c").write_text(EXTENSION_EXAMPLE, encoding="utf-8")451    build_dir = tmp_path.joinpath("build")452    dist_dir = tmp_path.joinpath("dist")453    monkeypatch.chdir(source_dir)454    bdist_wheel_cmd(bdist_dir=str(build_dir), dist_dir=str(dist_dir)).run()455 456 457def test_build_from_readonly_tree(dummy_dist, monkeypatch, tmp_path):458    basedir = str(tmp_path.joinpath("dummy"))459    shutil.copytree(str(dummy_dist), basedir)460    monkeypatch.chdir(basedir)461 462    # Make the tree read-only463    for root, _dirs, files in os.walk(basedir):464        for fname in files:465            os.chmod(os.path.join(root, fname), stat.S_IREAD)466 467    bdist_wheel_cmd().run()468 469 470@pytest.mark.parametrize(471    ("option", "compress_type"),472    list(bdist_wheel.supported_compressions.items()),473    ids=list(bdist_wheel.supported_compressions),474)475def test_compression(dummy_dist, monkeypatch, tmp_path, option, compress_type):476    monkeypatch.chdir(dummy_dist)477    bdist_wheel_cmd(bdist_dir=str(tmp_path), compression=option).run()478    with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:479        filenames = set(wf.namelist())480        assert "dummy_dist-1.0.dist-info/RECORD" in filenames481        assert "dummy_dist-1.0.dist-info/METADATA" in filenames482        for zinfo in wf.filelist:483            assert zinfo.compress_type == compress_type484 485 486def test_wheelfile_line_endings(wheel_paths):487    for path in wheel_paths:488        with ZipFile(path) as wf:489            wheelfile = next(fn for fn in wf.filelist if fn.filename.endswith("WHEEL"))490            wheelfile_contents = wf.read(wheelfile)491            assert b"\r" not in wheelfile_contents492 493 494def test_unix_epoch_timestamps(dummy_dist, monkeypatch, tmp_path):495    monkeypatch.setenv("SOURCE_DATE_EPOCH", "0")496    monkeypatch.chdir(dummy_dist)497    bdist_wheel_cmd(bdist_dir=str(tmp_path), build_number="2a").run()498    with ZipFile("dist/dummy_dist-1.0-2a-py3-none-any.whl") as wf:499        for zinfo in wf.filelist:500            assert zinfo.date_time >= (1980, 1, 1, 0, 0, 0)  # min epoch is used501 502 503def test_get_abi_tag_windows(monkeypatch):504    monkeypatch.setattr(tags, "interpreter_name", lambda: "cp")505    monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "cp313-win_amd64")506    assert get_abi_tag() == "cp313"507    monkeypatch.setattr(sys, "gettotalrefcount", lambda: 1, False)508    assert get_abi_tag() == "cp313d"509    monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "cp313t-win_amd64")510    assert get_abi_tag() == "cp313td"511    monkeypatch.delattr(sys, "gettotalrefcount")512    assert get_abi_tag() == "cp313t"513 514 515def test_get_abi_tag_pypy_old(monkeypatch):516    monkeypatch.setattr(tags, "interpreter_name", lambda: "pp")517    monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy36-pp73")518    assert get_abi_tag() == "pypy36_pp73"519 520 521def test_get_abi_tag_pypy_new(monkeypatch):522    monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy37-pp73-darwin")523    monkeypatch.setattr(tags, "interpreter_name", lambda: "pp")524    assert get_abi_tag() == "pypy37_pp73"525 526 527def test_get_abi_tag_graalpy(monkeypatch):528    monkeypatch.setattr(529        sysconfig, "get_config_var", lambda x: "graalpy231-310-native-x86_64-linux"530    )531    monkeypatch.setattr(tags, "interpreter_name", lambda: "graalpy")532    assert get_abi_tag() == "graalpy231_310_native"533 534 535def test_get_abi_tag_fallback(monkeypatch):536    monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "unknown-python-310")537    monkeypatch.setattr(tags, "interpreter_name", lambda: "unknown-python")538    assert get_abi_tag() == "unknown_python_310"539 540 541def test_platform_with_space(dummy_dist, monkeypatch):542    """Ensure building on platforms with a space in the name succeed."""543    monkeypatch.chdir(dummy_dist)544    bdist_wheel_cmd(plat_name="isilon onefs").run()545 546 547def test_data_dir_with_tag_build(monkeypatch, tmp_path):548    """549    Setuptools allow authors to set PEP 440's local version segments550    using ``egg_info.tag_build``. This should be reflected not only in the551    ``.whl`` file name, but also in the ``.dist-info`` and ``.data`` dirs.552    See pypa/setuptools#3997.553    """554    monkeypatch.chdir(tmp_path)555    files = {556        "setup.py": """557            from setuptools import setup558            setup(headers=["hello.h"])559            """,560        "setup.cfg": """561            [metadata]562            name = test563            version = 1.0564 565            [options.data_files]566            hello/world = file.txt567 568            [egg_info]569            tag_build = +what570            tag_date = 0571            """,572        "file.txt": "",573        "hello.h": "",574    }575    for file, content in files.items():576        with open(file, "w", encoding="utf-8") as fh:577            fh.write(cleandoc(content))578 579    bdist_wheel_cmd().run()580 581    # Ensure .whl, .dist-info and .data contain the local segment582    wheel_path = "dist/test-1.0+what-py3-none-any.whl"583    assert os.path.exists(wheel_path)584    entries = set(ZipFile(wheel_path).namelist())585    for expected in (586        "test-1.0+what.data/headers/hello.h",587        "test-1.0+what.data/data/hello/world/file.txt",588        "test-1.0+what.dist-info/METADATA",589        "test-1.0+what.dist-info/WHEEL",590    ):591        assert expected in entries592 593    for not_expected in (594        "test.data/headers/hello.h",595        "test-1.0.data/data/hello/world/file.txt",596        "test.dist-info/METADATA",597        "test-1.0.dist-info/WHEEL",598    ):599        assert not_expected not in entries600 601 602@pytest.mark.parametrize(603    ("reported", "expected"),604    [("linux-x86_64", "linux_i686"), ("linux-aarch64", "linux_armv7l")],605)606@pytest.mark.skipif(607    platform.system() != "Linux", reason="Only makes sense to test on Linux"608)609def test_platform_linux32(reported, expected, monkeypatch):610    monkeypatch.setattr(struct, "calcsize", lambda x: 4)611    dist = setuptools.Distribution()612    cmd = bdist_wheel(dist)613    cmd.plat_name = reported614    cmd.root_is_pure = False615    _, _, actual = cmd.get_tag()616    assert actual == expected617 618 619def test_no_ctypes(monkeypatch) -> None:620    def _fake_import(name: str, *args, **kwargs):621        if name == "ctypes":622            raise ModuleNotFoundError(f"No module named {name}")623 624        return importlib.__import__(name, *args, **kwargs)625 626    with suppress(KeyError):627        monkeypatch.delitem(sys.modules, "wheel.macosx_libfile")628 629    # Install an importer shim that refuses to load ctypes630    monkeypatch.setattr(builtins, "__import__", _fake_import)631    with pytest.raises(ModuleNotFoundError, match="No module named ctypes"):632        import wheel.macosx_libfile  # noqa: F401633 634    # Unload and reimport the bdist_wheel command module to make sure it won't try to635    # import ctypes636    monkeypatch.delitem(sys.modules, "setuptools.command.bdist_wheel")637 638    import setuptools.command.bdist_wheel  # noqa: F401639 640 641def test_dist_info_provided(dummy_dist, monkeypatch, tmp_path):642    monkeypatch.chdir(dummy_dist)643    distinfo = tmp_path / "dummy_dist.dist-info"644 645    distinfo.mkdir()646    (distinfo / "METADATA").write_text("name: helloworld", encoding="utf-8")647 648    # We don't control the metadata. According to PEP-517, "The hook MAY also649    # create other files inside this directory, and a build frontend MUST650    # preserve".651    (distinfo / "FOO").write_text("bar", encoding="utf-8")652 653    bdist_wheel_cmd(bdist_dir=str(tmp_path), dist_info_dir=str(distinfo)).run()654    expected = {655        "dummy_dist-1.0.dist-info/FOO",656        "dummy_dist-1.0.dist-info/RECORD",657    }658    with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:659        files_found = set(wf.namelist())660    # Check that all expected files are there.661    assert expected - files_found == set()662    # Make sure there is no accidental egg-info bleeding into the wheel.663    assert not [path for path in files_found if 'egg-info' in str(path)]664 665 666def test_allow_grace_period_parent_directory_license(monkeypatch, tmp_path):667    # Motivation: https://github.com/pypa/setuptools/issues/4892668    # TODO: Remove this test after deprecation period is over669    files = {670        "LICENSE.txt": "parent license",  # <---- the license files are outside671        "NOTICE.txt": "parent notice",672        "python": {673            "pyproject.toml": cleandoc(674                """675                [project]676                name = "test-proj"677                dynamic = ["version"]      # <---- testing dynamic will not break678                [tool.setuptools.dynamic]679                version.file = "VERSION"680                """681            ),682            "setup.cfg": cleandoc(683                """684                [metadata]685                license_files =686                  ../LICENSE.txt687                  ../NOTICE.txt688                """689            ),690            "VERSION": "42",691        },692    }693    jaraco.path.build(files, prefix=str(tmp_path))694    monkeypatch.chdir(tmp_path / "python")695    msg = "Pattern '../.*.txt' cannot contain '..'"696    with pytest.warns(SetuptoolsDeprecationWarning, match=msg):697        bdist_wheel_cmd().run()698    with ZipFile("dist/test_proj-42-py3-none-any.whl") as wf:699        files_found = set(wf.namelist())700        expected_files = {701            "test_proj-42.dist-info/licenses/LICENSE.txt",702            "test_proj-42.dist-info/licenses/NOTICE.txt",703        }704        assert expected_files <= files_found705 706        metadata = wf.read("test_proj-42.dist-info/METADATA").decode("utf8")707        assert "License-File: LICENSE.txt" in metadata708        assert "License-File: NOTICE.txt" in metadata709 
Aluode/PerceptionLabPortable · CoolFace