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.
- iterable-io-1.0.0/PKG-INFO +101 -0
- iterable-io-1.0.0/README.md +79 -0
- iterable-io-1.0.0/iterable_io.egg-info/PKG-INFO +101 -0
- iterable-io-1.0.0/iterable_io.egg-info/SOURCES.txt +8 -0
- iterable-io-1.0.0/iterable_io.egg-info/dependency_links.txt +1 -0
- iterable-io-1.0.0/iterable_io.egg-info/top_level.txt +1 -0
- iterable-io-1.0.0/iterableio.py +168 -0
- iterable-io-1.0.0/setup.cfg +4 -0
- iterable-io-1.0.0/setup.py +42 -0
- iterable-io-1.0.0/tests/test_iteratorio.py +170 -0
|
@@ -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
|
+
[](https://github.com/pR0Ps/iterable-io/actions/workflows/tests.yml)
|
|
26
|
+
[](https://pypi.org/project/iterable-io/)
|
|
27
|
+

|
|
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
|
+
[](https://github.com/pR0Ps/iterable-io/actions/workflows/tests.yml)
|
|
4
|
+
[](https://pypi.org/project/iterable-io/)
|
|
5
|
+

|
|
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
|
+
[](https://github.com/pR0Ps/iterable-io/actions/workflows/tests.yml)
|
|
26
|
+
[](https://pypi.org/project/iterable-io/)
|
|
27
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|