Aluode/PerceptionLabPortable
0
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 