diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33b64d7..c89a46b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,12 +29,9 @@ jobs: - name: Install run: | python -m pip install --upgrade pip - make setup pip install -U . - name: Test - run: make test - - name: Lint - run: make lint + run: python -m metadata_please.tests metadata_please: runs-on: ${{ matrix.os }} diff --git a/metadata_please/sdist.py b/metadata_please/sdist.py index c2bd5c2..743d673 100644 --- a/metadata_please/sdist.py +++ b/metadata_please/sdist.py @@ -12,12 +12,15 @@ def from_zip_sdist(zf: ZipFile) -> bytes: """ requires = [f for f in zf.namelist() if f.endswith("/requires.txt")] requires.sort(key=len) + if not requires: + return b"" + data = zf.read(requires[0]) assert data is not None - requires, extras = convert_sdist_requires(data.decode("utf-8")) + requires_lines, extras = convert_sdist_requires(data.decode("utf-8")) buf: list[str] = [] - for req in requires: + for req in requires_lines: buf.append(f"Requires-Dist: {req}\n") for extra in sorted(extras): buf.append(f"Provides-Extra: {extra}\n") @@ -27,6 +30,9 @@ def from_zip_sdist(zf: ZipFile) -> bytes: def basic_metadata_from_zip_sdist(zf: ZipFile) -> BasicMetadata: requires = [f for f in zf.namelist() if f.endswith("/requires.txt")] requires.sort(key=len) + if not requires: + return BasicMetadata((), frozenset()) + data = zf.read(requires[0]) assert data is not None return BasicMetadata.from_sdist_pkg_info_and_requires(b"", data) @@ -39,14 +45,16 @@ def from_tar_sdist(tf: TarFile) -> bytes: # XXX Why do ZipFile and TarFile not have a common interface ?! requires = [f for f in tf.getnames() if f.endswith("/requires.txt")] requires.sort(key=len) + if not requires: + return b"" fo = tf.extractfile(requires[0]) assert fo is not None - requires, extras = convert_sdist_requires(fo.read().decode("utf-8")) + requires_lines, extras = convert_sdist_requires(fo.read().decode("utf-8")) buf: list[str] = [] - for req in requires: + for req in requires_lines: buf.append(f"Requires-Dist: {req}\n") for extra in sorted(extras): buf.append(f"Provides-Extra: {extra}\n") @@ -57,6 +65,8 @@ def basic_metadata_from_tar_sdist(tf: TarFile) -> BasicMetadata: # XXX Why do ZipFile and TarFile not have a common interface ?! requires = [f for f in tf.getnames() if f.endswith("/requires.txt")] requires.sort(key=len) + if not requires: + return BasicMetadata((), frozenset()) fo = tf.extractfile(requires[0]) assert fo is not None diff --git a/metadata_please/tests/sdist.py b/metadata_please/tests/sdist.py index e0ecd93..529b29e 100644 --- a/metadata_please/tests/sdist.py +++ b/metadata_please/tests/sdist.py @@ -41,11 +41,23 @@ def test_basic_metadata(self) -> None: ) bm = basic_metadata_from_zip_sdist(z) # type: ignore self.assertEqual( - ["a", "b; extra == 'e'"], + ("a", "b; extra == 'e'"), bm.reqs, ) self.assertEqual({"e"}, bm.provides_extra) + def test_basic_metadata_no_requires_file(self) -> None: + z = MemoryZipFile( + ["foo.egg-info/PKG-INFO", "foo/__init__.py"], + read_value=b"\n", + ) + bm = basic_metadata_from_zip_sdist(z) # type: ignore + self.assertEqual( + (), + bm.reqs, + ) + self.assertEqual(set(), bm.provides_extra) + def test_basic_metadata_absl_py_09(self) -> None: z = MemoryZipFile( ["foo.egg-info/requires.txt", "foo/__init__.py"], @@ -60,12 +72,12 @@ def test_basic_metadata_absl_py_09(self) -> None: ) bm = basic_metadata_from_zip_sdist(z) # type: ignore self.assertEqual( - [ + ( "six", 'enum34; python_version < "3.4"', # Quoting on the following line is an implementation detail "pytest; (python_version < \"3.4\") and extra == 'test'", - ], + ), bm.reqs, ) self.assertEqual({"test"}, bm.provides_extra) @@ -102,7 +114,7 @@ def test_basic_metadata(self) -> None: ) bm = basic_metadata_from_tar_sdist(t) # type: ignore self.assertEqual( - ["a", "b; extra == 'e'"], + ("a", "b; extra == 'e'"), bm.reqs, ) self.assertEqual({"e"}, bm.provides_extra) diff --git a/metadata_please/types.py b/metadata_please/types.py index 12a0fcf..368f83e 100644 --- a/metadata_please/types.py +++ b/metadata_please/types.py @@ -11,14 +11,14 @@ class BasicMetadata: # Popualted from Requires-Dist or requires.txt reqs: Sequence[str] # Populated from Provides-Extra - provides_extra: set[str] + provides_extra: frozenset[str] @classmethod def from_metadata(cls, metadata: bytes) -> BasicMetadata: msg = message_from_string(metadata.decode("utf-8")) return BasicMetadata( msg.get_all("Requires-Dist") or (), - set(msg.get_all("Provides-Extra") or ()), + frozenset(msg.get_all("Provides-Extra") or ()), ) @classmethod @@ -33,7 +33,7 @@ def from_sdist_pkg_info_and_requires( ) -def convert_sdist_requires(data: str) -> tuple[list[str], set[str]]: +def convert_sdist_requires(data: str) -> tuple[tuple[str, ...], frozenset[str]]: # This is reverse engineered from looking at a couple examples, but there # does not appear to be a formal spec. Mentioned at # https://setuptools.readthedocs.io/en/latest/formats.html#requires-txt @@ -64,4 +64,4 @@ def convert_sdist_requires(data: str) -> tuple[list[str], set[str]]: lst.append(f"{line}; {current_markers}") else: lst.append(line) - return lst, extras + return tuple(lst), frozenset(extras)