Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions crates/spliterator-py/python/spliterator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
import os
from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Mapping
from importlib.metadata import PackageNotFoundError, version
from typing import Any, BinaryIO, Literal, TypeAlias, TypedDict, TypeVar, Unpack
from typing import Any, Literal, TypeAlias, TypedDict, TypeVar, Unpack

from . import _native
from ._errors import DuplicateHeaderError, JSONRowError, SpliteratorError, UnterminatedQuoteError
from ._types import SupportsRead

__all__ = [
"CSVOptions",
Expand All @@ -35,6 +36,7 @@
"JSONRowError",
"Source",
"SpliteratorError",
"SupportsRead",
"TextOptions",
"UnterminatedQuoteError",
"aread_csv",
Expand All @@ -58,8 +60,8 @@
except PackageNotFoundError: # pragma: no cover - a source checkout without an install
__version__ = "0.0.0"

Source: TypeAlias = str | os.PathLike[str] | bytes | bytearray | memoryview | BinaryIO
"""A path, bytes-like data, or a binary file object."""
Source: TypeAlias = str | os.PathLike[str] | bytes | bytearray | memoryview | SupportsRead
"""A path, bytes-like data, or a binary stream (anything with ``read(size) -> bytes``)."""

Mode: TypeAlias = Literal["object", "array", "entries"]

Expand Down Expand Up @@ -207,7 +209,7 @@ def _merge(defaults: Mapping[str, Any], options: Mapping[str, Any], reader: str)
return {**defaults, **options}


def _source(source: Source) -> str | bytes | BinaryIO:
def _source(source: Source) -> str | bytes | SupportsRead:
if isinstance(source, (bytes, bytearray, memoryview)):
return bytes(source)

Expand Down
6 changes: 4 additions & 2 deletions crates/spliterator-py/python/spliterator/_native.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from collections.abc import Iterator
from typing import Any, BinaryIO, TypeAlias
from typing import Any, TypeAlias

_Source: TypeAlias = str | bytes | BinaryIO
from ._types import SupportsRead

_Source: TypeAlias = str | bytes | SupportsRead

class TextBatches(Iterator[list[str]]):
def __init__(
Expand Down
16 changes: 16 additions & 0 deletions crates/spliterator-py/python/spliterator/_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Types shared by the public module and the native stubs."""

from __future__ import annotations

from typing import Protocol


class SupportsRead(Protocol):
"""A binary stream: anything with ``read(size) -> bytes``.

Structural rather than ``BinaryIO``, so the objects the standard library hands out type-check as they are: an
``open(path, "rb")`` handle, ``io.BytesIO``, a ``zipfile.ZipFile.open`` member, ``gzip.open``, a socket's
``makefile("rb")``.
"""

def read(self, size: int = ..., /) -> bytes: ...
12 changes: 12 additions & 0 deletions crates/spliterator-py/tests/test_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import asyncio
import gzip
import io
import zipfile
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -144,3 +145,14 @@ def test_counts_match_reads(tmp_path: Path) -> None:
assert spliterator.count_text(path) == 5000
assert spliterator.count_text(path, drop=10, take=7) == 7
assert spliterator.count_csv(b"k,k\n1,2\n3,4\n") == 2


def test_a_zip_member_is_a_source(tmp_path: Path) -> None:
archive_path = tmp_path / "data.zip"

with zipfile.ZipFile(archive_path, "w") as archive:
archive.writestr("rows.csv", "a,b\n1,2\n")

with zipfile.ZipFile(archive_path) as archive, archive.open("rows.csv") as member:
# `ZipFile.open` returns `IO[bytes]`, not `BinaryIO`; the source type must accept it as it is.
assert list(spliterator.read_csv(member)) == [{"a": "1", "b": "2"}]
Loading