iterable-io 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.1
2
+ Name: iterable-io
3
+ Version: 1.0.0
4
+ Summary: Adapt generators and other iterables to a file-like interface
5
+ Home-page: https://github.com/pR0Ps/iterable-io
6
+ License: LGPLv3
7
+ Project-URL: Source, https://github.com/pR0Ps/iterable-io
8
+ Project-URL: Changelog, https://github.com/pR0Ps/iterable-io/blob/master/CHANGELOG.md
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.5
11
+ Classifier: Programming Language :: Python :: 3.6
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
20
+ Requires-Python: >=3.5
21
+ Description-Content-Type: text/markdown
22
+
23
+ iterable-io
24
+ ===========
25
+ [![Status](https://github.com/pR0Ps/iterable-io/workflows/tests/badge.svg)](https://github.com/pR0Ps/iterable-io/actions/workflows/tests.yml)
26
+ [![Version](https://img.shields.io/pypi/v/iterable-io.svg)](https://pypi.org/project/iterable-io/)
27
+ ![Python](https://img.shields.io/pypi/pyversions/iterable-io.svg)
28
+
29
+ `iterable-io` is a small Python library that provides an adapter so that it's possible to read from
30
+ [iterable](https://docs.python.org/3/glossary.html#term-iterable) objects in the same way as
31
+ [file-like](https://docs.python.org/3/glossary.html#term-file-object) objects.
32
+
33
+ It is primarily useful as "glue" between two incompatible interfaces. As an example, in the case
34
+ where one interface expects a file-like object to call `.read()` on, and the other only provides a
35
+ generator of bytes.
36
+
37
+ One way to solve this issue would be to write all the bytes in the generator to a temporary file,
38
+ then provide that file instead, but if the generator produces a large amount of data then this is
39
+ both slow to start, and resource-intensive.
40
+
41
+ This library allows streaming data between these two incompatible interfaces so as data is requested
42
+ by `.read()`, it's pulled from the iterable. This keeps resource usage low and removes the startup
43
+ delay.
44
+
45
+
46
+ Installation
47
+ ------------
48
+ ```
49
+ pip install iterable-io
50
+ ```
51
+
52
+
53
+ Documentation
54
+ -------------
55
+ The functionality of this library is accessed via a single function: `open_iterable()`.
56
+
57
+ `open_iterable()` is designed to work the same was as the builtin `open()`, except that it takes an
58
+ iterable to "open" instead of a file. For example, it can open the iterable in binary or text mode,
59
+ has options for buffering, encoding, etc. See the docstring of `open_iterable` for more detailed
60
+ documentation.
61
+
62
+
63
+ Simple examples
64
+ ---------------
65
+ The following examples should be enough to understand in which cases `open_iterable()` would be
66
+ useful and get a high-level understanding of how to use it:
67
+
68
+ Read bytes from a generator of bytes:
69
+ ```python
70
+ gen = generate_bytes()
71
+
72
+ # adapt the generator to a file-like object in binary mode
73
+ # (fp.read() will return bytes)
74
+ fp = open_iterable(gen, "rb")
75
+
76
+ while chunk := fp.read(4096):
77
+ process_chunk(chunk)
78
+ ```
79
+
80
+ Read lines of text from a generator of bytes:
81
+ ```python
82
+ gen = generate_bytes()
83
+
84
+ # adapt the generator to a file-like object in text mode
85
+ # (fp.read() will return a string, fp.readline is also available)
86
+ fp = open_iterable(gen, "rt", encoding="utf-8")
87
+
88
+ for line in fp:
89
+ process_line_of_text(line)
90
+ ```
91
+
92
+
93
+ Tests
94
+ -----
95
+ This package contains extensive tests. To run them, install `pytest` (`pip install pytest`) and run
96
+ `py.test` in the project directory.
97
+
98
+
99
+ License
100
+ -------
101
+ Licensed under the [GNU LGPLv3](https://www.gnu.org/licenses/lgpl-3.0.html).
@@ -0,0 +1,79 @@
1
+ iterable-io
2
+ ===========
3
+ [![Status](https://github.com/pR0Ps/iterable-io/workflows/tests/badge.svg)](https://github.com/pR0Ps/iterable-io/actions/workflows/tests.yml)
4
+ [![Version](https://img.shields.io/pypi/v/iterable-io.svg)](https://pypi.org/project/iterable-io/)
5
+ ![Python](https://img.shields.io/pypi/pyversions/iterable-io.svg)
6
+
7
+ `iterable-io` is a small Python library that provides an adapter so that it's possible to read from
8
+ [iterable](https://docs.python.org/3/glossary.html#term-iterable) objects in the same way as
9
+ [file-like](https://docs.python.org/3/glossary.html#term-file-object) objects.
10
+
11
+ It is primarily useful as "glue" between two incompatible interfaces. As an example, in the case
12
+ where one interface expects a file-like object to call `.read()` on, and the other only provides a
13
+ generator of bytes.
14
+
15
+ One way to solve this issue would be to write all the bytes in the generator to a temporary file,
16
+ then provide that file instead, but if the generator produces a large amount of data then this is
17
+ both slow to start, and resource-intensive.
18
+
19
+ This library allows streaming data between these two incompatible interfaces so as data is requested
20
+ by `.read()`, it's pulled from the iterable. This keeps resource usage low and removes the startup
21
+ delay.
22
+
23
+
24
+ Installation
25
+ ------------
26
+ ```
27
+ pip install iterable-io
28
+ ```
29
+
30
+
31
+ Documentation
32
+ -------------
33
+ The functionality of this library is accessed via a single function: `open_iterable()`.
34
+
35
+ `open_iterable()` is designed to work the same was as the builtin `open()`, except that it takes an
36
+ iterable to "open" instead of a file. For example, it can open the iterable in binary or text mode,
37
+ has options for buffering, encoding, etc. See the docstring of `open_iterable` for more detailed
38
+ documentation.
39
+
40
+
41
+ Simple examples
42
+ ---------------
43
+ The following examples should be enough to understand in which cases `open_iterable()` would be
44
+ useful and get a high-level understanding of how to use it:
45
+
46
+ Read bytes from a generator of bytes:
47
+ ```python
48
+ gen = generate_bytes()
49
+
50
+ # adapt the generator to a file-like object in binary mode
51
+ # (fp.read() will return bytes)
52
+ fp = open_iterable(gen, "rb")
53
+
54
+ while chunk := fp.read(4096):
55
+ process_chunk(chunk)
56
+ ```
57
+
58
+ Read lines of text from a generator of bytes:
59
+ ```python
60
+ gen = generate_bytes()
61
+
62
+ # adapt the generator to a file-like object in text mode
63
+ # (fp.read() will return a string, fp.readline is also available)
64
+ fp = open_iterable(gen, "rt", encoding="utf-8")
65
+
66
+ for line in fp:
67
+ process_line_of_text(line)
68
+ ```
69
+
70
+
71
+ Tests
72
+ -----
73
+ This package contains extensive tests. To run them, install `pytest` (`pip install pytest`) and run
74
+ `py.test` in the project directory.
75
+
76
+
77
+ License
78
+ -------
79
+ Licensed under the [GNU LGPLv3](https://www.gnu.org/licenses/lgpl-3.0.html).
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.1
2
+ Name: iterable-io
3
+ Version: 1.0.0
4
+ Summary: Adapt generators and other iterables to a file-like interface
5
+ Home-page: https://github.com/pR0Ps/iterable-io
6
+ License: LGPLv3
7
+ Project-URL: Source, https://github.com/pR0Ps/iterable-io
8
+ Project-URL: Changelog, https://github.com/pR0Ps/iterable-io/blob/master/CHANGELOG.md
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.5
11
+ Classifier: Programming Language :: Python :: 3.6
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
20
+ Requires-Python: >=3.5
21
+ Description-Content-Type: text/markdown
22
+
23
+ iterable-io
24
+ ===========
25
+ [![Status](https://github.com/pR0Ps/iterable-io/workflows/tests/badge.svg)](https://github.com/pR0Ps/iterable-io/actions/workflows/tests.yml)
26
+ [![Version](https://img.shields.io/pypi/v/iterable-io.svg)](https://pypi.org/project/iterable-io/)
27
+ ![Python](https://img.shields.io/pypi/pyversions/iterable-io.svg)
28
+
29
+ `iterable-io` is a small Python library that provides an adapter so that it's possible to read from
30
+ [iterable](https://docs.python.org/3/glossary.html#term-iterable) objects in the same way as
31
+ [file-like](https://docs.python.org/3/glossary.html#term-file-object) objects.
32
+
33
+ It is primarily useful as "glue" between two incompatible interfaces. As an example, in the case
34
+ where one interface expects a file-like object to call `.read()` on, and the other only provides a
35
+ generator of bytes.
36
+
37
+ One way to solve this issue would be to write all the bytes in the generator to a temporary file,
38
+ then provide that file instead, but if the generator produces a large amount of data then this is
39
+ both slow to start, and resource-intensive.
40
+
41
+ This library allows streaming data between these two incompatible interfaces so as data is requested
42
+ by `.read()`, it's pulled from the iterable. This keeps resource usage low and removes the startup
43
+ delay.
44
+
45
+
46
+ Installation
47
+ ------------
48
+ ```
49
+ pip install iterable-io
50
+ ```
51
+
52
+
53
+ Documentation
54
+ -------------
55
+ The functionality of this library is accessed via a single function: `open_iterable()`.
56
+
57
+ `open_iterable()` is designed to work the same was as the builtin `open()`, except that it takes an
58
+ iterable to "open" instead of a file. For example, it can open the iterable in binary or text mode,
59
+ has options for buffering, encoding, etc. See the docstring of `open_iterable` for more detailed
60
+ documentation.
61
+
62
+
63
+ Simple examples
64
+ ---------------
65
+ The following examples should be enough to understand in which cases `open_iterable()` would be
66
+ useful and get a high-level understanding of how to use it:
67
+
68
+ Read bytes from a generator of bytes:
69
+ ```python
70
+ gen = generate_bytes()
71
+
72
+ # adapt the generator to a file-like object in binary mode
73
+ # (fp.read() will return bytes)
74
+ fp = open_iterable(gen, "rb")
75
+
76
+ while chunk := fp.read(4096):
77
+ process_chunk(chunk)
78
+ ```
79
+
80
+ Read lines of text from a generator of bytes:
81
+ ```python
82
+ gen = generate_bytes()
83
+
84
+ # adapt the generator to a file-like object in text mode
85
+ # (fp.read() will return a string, fp.readline is also available)
86
+ fp = open_iterable(gen, "rt", encoding="utf-8")
87
+
88
+ for line in fp:
89
+ process_line_of_text(line)
90
+ ```
91
+
92
+
93
+ Tests
94
+ -----
95
+ This package contains extensive tests. To run them, install `pytest` (`pip install pytest`) and run
96
+ `py.test` in the project directory.
97
+
98
+
99
+ License
100
+ -------
101
+ Licensed under the [GNU LGPLv3](https://www.gnu.org/licenses/lgpl-3.0.html).
@@ -0,0 +1,8 @@
1
+ README.md
2
+ iterableio.py
3
+ setup.py
4
+ iterable_io.egg-info/PKG-INFO
5
+ iterable_io.egg-info/SOURCES.txt
6
+ iterable_io.egg-info/dependency_links.txt
7
+ iterable_io.egg-info/top_level.txt
8
+ tests/test_iteratorio.py
@@ -0,0 +1 @@
1
+ iterableio
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python
2
+
3
+ import io
4
+
5
+
6
+ class RawIterableReader(io.RawIOBase):
7
+ """A io.RawIOBase implemention for an iterable of bytes
8
+
9
+ In most cases, this class should not be used directly. See the included
10
+ `open_iterable` function for a high-level interface.
11
+ """
12
+
13
+ def __init__(self, iterable):
14
+ self._iter = iter(iterable)
15
+ self._extra = bytearray()
16
+ self._total = 0
17
+
18
+ def readable(self):
19
+ return True
20
+
21
+ def close(self):
22
+ self._iter = None
23
+ super().close()
24
+
25
+ def tell(self):
26
+ """The total number of bytes that have been read"""
27
+ self._checkClosed()
28
+ return self._total - len(self._extra)
29
+
30
+ def readinto(self, b):
31
+ """Read bytes into a pre-allocated bytes-like object b
32
+
33
+ Returns the number of bytes read, 0 indicates EOF
34
+ """
35
+ self._checkClosed()
36
+ num = len(b)
37
+ if self._iter is not None:
38
+ while len(self._extra) < num:
39
+ try:
40
+ new = next(self._iter)
41
+ except StopIteration:
42
+ self._iter = None
43
+ break
44
+ else:
45
+ self._total += len(new)
46
+ self._extra += new
47
+
48
+ ret, self._extra = self._extra[:num], self._extra[num:]
49
+
50
+ lret = len(ret)
51
+ b[:lret] = ret
52
+ return lret
53
+
54
+
55
+ def open_iterable(iterable, mode="r", buffering=-1, encoding=None, errors=None, newline=None):
56
+ """Open an iterable of bytes to read from it using a file-like interface
57
+
58
+ The `iterable` must be an iterable of bytes.
59
+
60
+ mode is an optional string that specifies the mode in which the file is
61
+ opened. It defaults to 'rt' which means open for reading in text mode. In
62
+ text mode, if encoding is not specified the encoding used is platform
63
+ dependent. (For reading raw bytes use binary mode and leave encoding
64
+ unspecified.) The available modes are:
65
+
66
+ ========= ===============================================================
67
+ Character Meaning
68
+ --------- ---------------------------------------------------------------
69
+ 'r' open for reading (default)
70
+ 'b' binary mode
71
+ 't' text mode (default)
72
+ ========= ===============================================================
73
+
74
+ Iterables opened in binary mode (appending 'b' to the mode argument) return
75
+ contents as bytes objects without any decoding. In text mode (the default),
76
+ the contents of the iterable are returned as strings, the bytes having been
77
+ first decoded using a platform-dependent encoding or using the specified
78
+ encoding if given.
79
+
80
+ buffering is an optional integer used to set the buffering policy. Pass 0
81
+ to switch buffering off (only allowed in binary mode), and an integer > 0
82
+ to indicate the size of a fixed-size chunk buffer. When no buffering
83
+ argument is given, `io.DEFAULT_BUFFER_SIZE` will be used. On many systems,
84
+ the buffer will typically be 4096 or 8192 bytes long.
85
+
86
+ encoding is the str name of the encoding used to decode or encode the
87
+ file. This should only be used in text mode. The default encoding is
88
+ platform dependent, but any encoding supported by Python can be
89
+ passed. See the codecs module for the list of supported encodings.
90
+
91
+ errors is an optional string that specifies how encoding errors are to
92
+ be handled---this argument should not be used in binary mode. Pass
93
+ 'strict' to raise a ValueError exception if there is an encoding error
94
+ (the default of None has the same effect), or pass 'ignore' to ignore
95
+ errors. Note that ignoring encoding errors can lead to data loss.
96
+ See the documentation for codecs.register for a list of the permitted
97
+ encoding error strings.
98
+
99
+ newline is a string controlling how universal newlines works (it only
100
+ applies to text mode). It can be None, '', '\n', '\r', and '\r\n'. It works
101
+ as follows:
102
+
103
+ * On input, if newline is None, universal newlines mode is
104
+ enabled. Lines in the input can end in '\n', '\r', or '\r\n', and
105
+ these are translated into '\n' before being returned to the
106
+ caller. If it is '', universal newline mode is enabled, but line
107
+ endings are returned to the caller untranslated. If it has any of
108
+ the other legal values, input lines are only terminated by the given
109
+ string, and the line ending is returned to the caller untranslated.
110
+
111
+ * On output, if newline is None, any '\n' characters written are
112
+ translated to the system default line separator, os.linesep. If
113
+ newline is '', no translation takes place. If newline is any of the
114
+ other legal values, any '\n' characters written are translated to
115
+ the given string.
116
+
117
+ open_iterable() returns a file object whose type depends on the mode, and
118
+ through which the standard file operations such as read() are performed.
119
+ When open_iterable() is used to open an iterable in a text mode ('rt'), it
120
+ returns an io.TextIOWrapper. When used to open an iterable in a binary
121
+ mode, the returned class varies: For unbuffered access, a RawIterableReader
122
+ is returned and in buffered mode it returns an io.BufferedReader.
123
+ """
124
+ # This function is modeled after `io.open`, found in `Lib/_pyio.py`
125
+
126
+ modes = set(mode)
127
+ if modes - set("rtb") or len(mode) > len(modes):
128
+ raise ValueError("invalid mode: '{}'".format(mode))
129
+
130
+ reading = "r" in modes
131
+ binary = "b" in modes
132
+ text = "t" in modes or (reading and not binary)
133
+
134
+ if not reading:
135
+ raise ValueError("Must specify read mode")
136
+ if text and binary:
137
+ raise ValueError("can't have text and binary mode at once")
138
+ if binary and encoding is not None:
139
+ raise ValueError("binary mode doesn't take an encoding argument")
140
+ if binary and errors is not None:
141
+ raise ValueError("binary mode doesn't take an errors argument")
142
+ if binary and newline is not None:
143
+ raise ValueError("binary mode doesn't take a newline argument")
144
+ if text and buffering == 0:
145
+ raise ValueError("can't have unbuffered text I/O")
146
+
147
+ ret = RawIterableReader(iterable)
148
+ try:
149
+ if buffering == 0:
150
+ # unbuffered binary mode
151
+ return ret
152
+
153
+ if buffering < 0:
154
+ buffering = io.DEFAULT_BUFFER_SIZE
155
+
156
+ ret = io.BufferedReader(ret, buffering)
157
+
158
+ if binary:
159
+ # buffered binary mode
160
+ return ret
161
+
162
+ # buffered text mode
163
+ ret = io.TextIOWrapper(ret, encoding, errors, newline)
164
+ ret.mode = mode
165
+ return ret
166
+ except:
167
+ ret.close()
168
+ raise
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python
2
+
3
+ from setuptools import setup
4
+ import os.path
5
+
6
+
7
+ try:
8
+ DIR = os.path.abspath(os.path.dirname(__file__))
9
+ with open(os.path.join(DIR, "README.md"), encoding='utf-8') as f:
10
+ long_description = f.read()
11
+ except Exception:
12
+ long_description=None
13
+
14
+
15
+ setup(
16
+ name="iterable-io",
17
+ version="1.0.0",
18
+ description="Adapt generators and other iterables to a file-like interface",
19
+ long_description=long_description,
20
+ long_description_content_type="text/markdown",
21
+ url="https://github.com/pR0Ps/iterable-io",
22
+ project_urls={
23
+ "Source": "https://github.com/pR0Ps/iterable-io",
24
+ "Changelog": "https://github.com/pR0Ps/iterable-io/blob/master/CHANGELOG.md",
25
+ },
26
+ license="LGPLv3",
27
+ classifiers=[
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.5",
30
+ "Programming Language :: Python :: 3.6",
31
+ "Programming Language :: Python :: 3.7",
32
+ "Programming Language :: Python :: 3.8",
33
+ "Programming Language :: Python :: 3.9",
34
+ "Programming Language :: Python :: 3.10",
35
+ "Programming Language :: Python :: 3.11",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Operating System :: OS Independent",
38
+ "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)"
39
+ ],
40
+ py_modules=["iterableio"],
41
+ python_requires=">=3.5",
42
+ )
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env python
2
+
3
+ import io
4
+
5
+ from iterableio import RawIterableReader, open_iterable
6
+
7
+ import pytest
8
+
9
+
10
+ @pytest.mark.parametrize("mode, buffering, encoding, errors, newline",[
11
+ # bad modes
12
+ ("", -1, None, None, None),
13
+ ("abc", -1, None, None, None),
14
+ ("rtb", -1, None, None, None),
15
+ ("rt", 0, None, None, None), # need buffering
16
+ ("rt", "bad int", None, None, None), # invalid buffering int
17
+ # can't provide text decoding params in binary mode
18
+ ("rb", 0, "utf-8", None, None),
19
+ ("rb", 0, None, "ignore", None),
20
+ ("rb", 0, None, None, "\n"),
21
+ ])
22
+ def test_invalid_input(mode, buffering, encoding, errors, newline):
23
+ """Test that invalid params are caught"""
24
+ with pytest.raises((ValueError, TypeError, LookupError)):
25
+ open_iterable([], mode, buffering, encoding, errors, newline)
26
+
27
+
28
+ @pytest.mark.parametrize("buffering", (0, -1, 1))
29
+ def test_reading(buffering):
30
+
31
+ def gen():
32
+ yield from (
33
+ b'\x01\x02\x03\x04\x05',
34
+ b"abcde",
35
+ b"fghij",
36
+ b"klmno",
37
+ b"qrstu",
38
+ b"vwxyz",
39
+ b'\x06\x07\x08\x09\x10',
40
+ )
41
+
42
+ _data = b"".join(gen())
43
+
44
+ with open_iterable(gen(), "rb", buffering=buffering) as i:
45
+ assert i.readable()
46
+ assert not i.seekable()
47
+ assert not i.writable()
48
+
49
+ cnt = 0
50
+ for amt in (0, 1, 2, 3, 4, 5, 10, 1, 1, 0):
51
+ d = i.read(amt)
52
+ assert len(d) == amt
53
+ assert d == _data[cnt:cnt+amt]
54
+ cnt += amt
55
+ assert i.tell() == cnt
56
+
57
+ assert i.read() == _data[cnt:]
58
+ assert i.read() == b""
59
+
60
+ assert i.tell() == len(_data)
61
+
62
+
63
+ def test_returned_class():
64
+ """Test that the correct class is returned depending on the mode and buffering spec"""
65
+ assert isinstance(open_iterable([], "rb", buffering=0), RawIterableReader)
66
+ assert isinstance(open_iterable([], "rb", buffering=-1), io.BufferedReader)
67
+ assert isinstance(open_iterable([], "rb", buffering=1), io.BufferedReader)
68
+ assert isinstance(open_iterable([], "rt", buffering=-1), io.TextIOWrapper)
69
+ assert isinstance(open_iterable([], "rt", buffering=1), io.TextIOWrapper)
70
+
71
+
72
+ @pytest.mark.parametrize("mode, buffering",[
73
+ ("rb", 0),
74
+ ("rb", -1),
75
+ ("rt", -1),
76
+ ])
77
+ def test_contextmgr_close(mode, buffering):
78
+ with open_iterable([], mode, buffering) as i:
79
+ assert not i.closed
80
+ assert i.closed
81
+
82
+
83
+ @pytest.mark.parametrize("mode, buffering",[
84
+ ("rb", 0),
85
+ ("rb", -1),
86
+ ("rt", -1),
87
+ ])
88
+ def test_unreadable_after_close(mode, buffering):
89
+
90
+ i = open_iterable([b"12345"], mode, buffering)
91
+ assert not i.read(0)
92
+ assert i.read(1) in (b"1", "1")
93
+ assert not i.closed
94
+
95
+ i.close()
96
+ assert i.closed
97
+
98
+ with pytest.raises(ValueError, match="closed"):
99
+ i.read()
100
+ with pytest.raises(ValueError, match="closed"):
101
+ i.tell()
102
+
103
+
104
+ def test_yield_empty_bytes():
105
+ """Test that a generator is only 'done' when it stops yielding, not when it yields empty bytes"""
106
+ def gen():
107
+ yield from (
108
+ b"1",
109
+ b"", b"", b"", b"", b"", b"", b"",
110
+ b"2", b"3",
111
+ b"", b"", b"", b"", b"", b"",
112
+ b"4",
113
+ )
114
+
115
+ i = RawIterableReader(gen())
116
+ out = []
117
+ while True:
118
+ b = i.read(1)
119
+ if not b:
120
+ break
121
+ out.append(b)
122
+
123
+ assert len(out) == 4
124
+ assert b"".join(out) == b"1234"
125
+
126
+
127
+ def test_read_text():
128
+ def gen():
129
+ # 9 lines yielded in non-line chunks
130
+ yield from (
131
+ x.encode("utf-8") for x in (
132
+ "this is a line\n",
133
+ "",
134
+ "",
135
+ "_a",
136
+ "another line\n",
137
+ "another line1\n",
138
+ "another line2\n",
139
+ "another line_",
140
+ "a",
141
+ "aaaaaaa\nbbbbbbbb",
142
+ "_",
143
+ "1",
144
+ "2",
145
+ "3",
146
+ "4",
147
+ "5",
148
+ "_line line line another line actually\n",
149
+ "another line\n",
150
+ "ending line\n",
151
+ "actual ending line no trailing newline",
152
+ )
153
+ )
154
+
155
+ real = "".join(x.decode("utf-8") for x in gen())
156
+
157
+ # read across chunks and lines
158
+ with open_iterable(gen(), encoding="utf-8") as i:
159
+ assert i.read(10) == real[:10]
160
+ assert i.read(10) == real[10:20]
161
+
162
+ with open_iterable(gen(), encoding="utf-8") as i:
163
+ lines = list(i)
164
+
165
+ with open_iterable(gen(), encoding="utf-8") as i:
166
+ assert lines == i.readlines()
167
+
168
+
169
+ assert len(lines) == len(real.splitlines()) == 9
170
+ assert "".join(lines) == real