Skip to content

Commit c689da0

Browse files
authored
Merge pull request #161 from pycompression/release_1.4.1
Release 1.4.1
2 parents 8e0b1ea + 15a4810 commit c689da0

File tree

8 files changed

+110
-62
lines changed

8 files changed

+110
-62
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ Changelog
77
.. This document is user facing. Please word the changes in such a way
88
.. that users understand how the changes affect the new version.
99
10+
version 1.4.1
11+
-----------------
12+
+ Fix several errors related to unclosed files and buffers.
13+
1014
version 1.4.0
1115
-----------------
1216
+ Drop support for python 3.7 and PyPy 3.8 as these are no longer supported.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def build_isa_l():
135135

136136
setup(
137137
name="isal",
138-
version="1.4.0",
138+
version="1.4.1",
139139
description="Faster zlib and gzip compatible compression and "
140140
"decompression by providing python bindings for the ISA-L "
141141
"library.",

src/isal/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@
2727
"__version__"
2828
]
2929

30-
__version__ = "1.4.0"
30+
__version__ = "1.4.1"

src/isal/igzip.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
Library to speed up its methods."""
3030

3131
import argparse
32+
import builtins
3233
import gzip
3334
import io
3435
import os
@@ -337,13 +338,14 @@ def main():
337338
if yes_or_no not in {"y", "Y", "yes"}:
338339
sys.exit("not overwritten")
339340

341+
out_buffer = None
340342
if args.compress:
341343
if args.file is None:
342344
in_file = sys.stdin.buffer
343345
else:
344-
in_file = io.open(args.file, mode="rb")
346+
in_file = builtins.open(args.file, mode="rb")
345347
if out_filepath is not None:
346-
out_buffer = io.open(out_filepath, "wb")
348+
out_buffer = builtins.open(out_filepath, "wb")
347349
else:
348350
out_buffer = sys.stdout.buffer
349351

@@ -359,7 +361,7 @@ def main():
359361
else:
360362
in_file = IGzipFile(mode="rb", fileobj=sys.stdin.buffer)
361363
if out_filepath is not None:
362-
out_file = io.open(out_filepath, mode="wb")
364+
out_file = builtins.open(out_filepath, mode="wb")
363365
else:
364366
out_file = sys.stdout.buffer
365367

@@ -374,6 +376,8 @@ def main():
374376
in_file.close()
375377
if out_file is not sys.stdout.buffer:
376378
out_file.close()
379+
if out_buffer is not None and out_buffer is not sys.stdout.buffer:
380+
out_buffer.close()
377381

378382

379383
if __name__ == "__main__": # pragma: no cover

src/isal/igzip_threaded.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# This file is part of python-isal which is distributed under the
66
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2.
77

8+
import builtins
89
import io
910
import multiprocessing
1011
import os
@@ -54,7 +55,7 @@ def open(filename, mode="rb", compresslevel=igzip._COMPRESS_LEVEL_TRADEOFF,
5455
threads = 1
5556
open_mode = mode.replace("t", "b")
5657
if isinstance(filename, (str, bytes)) or hasattr(filename, "__fspath__"):
57-
binary_file = io.open(filename, open_mode)
58+
binary_file = builtins.open(filename, open_mode)
5859
elif hasattr(filename, "read") or hasattr(filename, "write"):
5960
binary_file = filename
6061
else:
@@ -83,9 +84,14 @@ def __init__(self, fp, queue_size=4, block_size=8 * 1024 * 1024):
8384
self.buffer = io.BytesIO()
8485
self.block_size = block_size
8586
self.worker = threading.Thread(target=self._decompress)
87+
self._closed = False
8688
self.running = True
8789
self.worker.start()
8890

91+
def _check_closed(self, msg=None):
92+
if self._closed:
93+
raise ValueError("I/O operation on closed file")
94+
8995
def _decompress(self):
9096
block_size = self.block_size
9197
block_queue = self.queue
@@ -105,6 +111,7 @@ def _decompress(self):
105111
pass
106112

107113
def readinto(self, b):
114+
self._check_closed()
108115
result = self.buffer.readinto(b)
109116
if result == 0:
110117
while True:
@@ -125,16 +132,22 @@ def readinto(self, b):
125132
def readable(self) -> bool:
126133
return True
127134

128-
def writable(self) -> bool:
129-
return False
130-
131135
def tell(self) -> int:
136+
self._check_closed()
132137
return self.pos
133138

134139
def close(self) -> None:
140+
if self._closed:
141+
return
135142
self.running = False
136143
self.worker.join()
137144
self.fileobj.close()
145+
self.raw.close()
146+
self._closed = True
147+
148+
@property
149+
def closed(self) -> bool:
150+
return self._closed
138151

139152

140153
class _ThreadedGzipWriter(io.RawIOBase):
@@ -199,6 +212,10 @@ def __init__(self,
199212
self._write_gzip_header()
200213
self.start()
201214

215+
def _check_closed(self, msg=None):
216+
if self._closed:
217+
raise ValueError("I/O operation on closed file")
218+
202219
def _write_gzip_header(self):
203220
"""Simple gzip header. Only xfl flag is set according to level."""
204221
magic1 = 0x1f
@@ -225,11 +242,10 @@ def stop(self):
225242
self.output_worker.join()
226243

227244
def write(self, b) -> int:
245+
self._check_closed()
228246
with self.lock:
229247
if self.exception:
230248
raise self.exception
231-
if self._closed:
232-
raise IOError("Can not write closed file")
233249
index = self.index
234250
data = bytes(b)
235251
zdict = memoryview(self.previous_block)[-DEFLATE_WINDOW_SIZE:]
@@ -240,8 +256,7 @@ def write(self, b) -> int:
240256
return len(data)
241257

242258
def flush(self):
243-
if self._closed:
244-
raise IOError("Can not write closed file")
259+
self._check_closed()
245260
# Wait for all data to be compressed
246261
for in_q in self.input_queues:
247262
in_q.join()
@@ -251,9 +266,13 @@ def flush(self):
251266
self.raw.flush()
252267

253268
def close(self) -> None:
269+
if self._closed:
270+
return
254271
self.flush()
255272
self.stop()
256273
if self.exception:
274+
self.raw.close()
275+
self._closed = True
257276
raise self.exception
258277
# Write an empty deflate block with a lost block marker.
259278
self.raw.write(isal_zlib.compress(b"", wbits=-15))

tests/test_igzip.py

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import re
1616
import shutil
1717
import struct
18-
import subprocess
1918
import sys
2019
import tempfile
2120
import zlib
@@ -29,21 +28,6 @@
2928
DATA = b'This is a simple test with igzip'
3029
COMPRESSED_DATA = gzip.compress(DATA)
3130
TEST_FILE = str((Path(__file__).parent / "data" / "test.fastq.gz"))
32-
PYPY = sys.implementation.name == "pypy"
33-
34-
35-
def run_isal_igzip(*args, stdin=None):
36-
"""Calling isal.igzip externally seems to solve some issues on PyPy where
37-
files would not be written properly when igzip.main() was called. This is
38-
probably due to some out of order execution that PyPy tries to pull.
39-
Running the process externally is detrimental to the coverage report,
40-
so this is only done for PyPy."""
41-
process = subprocess.Popen(["python", "-m", "isal.igzip", *args],
42-
stdout=subprocess.PIPE,
43-
stderr=subprocess.PIPE,
44-
stdin=subprocess.PIPE)
45-
46-
return process.communicate(stdin)
4731

4832

4933
def test_wrong_compresslevel_igzipfile():
@@ -128,12 +112,9 @@ def test_decompress_infile_outfile(tmp_path, capsysbinary):
128112
def test_compress_infile_outfile(tmp_path, capsysbinary):
129113
test_file = tmp_path / "test"
130114
test_file.write_bytes(DATA)
131-
if PYPY:
132-
out, err = run_isal_igzip(str(test_file))
133-
else:
134-
sys.argv = ['', str(test_file)]
135-
igzip.main()
136-
out, err = capsysbinary.readouterr()
115+
sys.argv = ['', str(test_file)]
116+
igzip.main()
117+
out, err = capsysbinary.readouterr()
137118
out_file = test_file.with_suffix(".gz")
138119
assert err == b''
139120
assert out == b''
@@ -196,12 +177,9 @@ def test_compress_infile_out_file(tmp_path, capsysbinary):
196177
test.write_bytes(DATA)
197178
out_file = tmp_path / "compressed.gz"
198179
args = ['-o', str(out_file), str(test)]
199-
if PYPY:
200-
out, err = run_isal_igzip(*args)
201-
else:
202-
sys.argv = ['', *args]
203-
igzip.main()
204-
out, err = capsysbinary.readouterr()
180+
sys.argv = ['', *args]
181+
igzip.main()
182+
out, err = capsysbinary.readouterr()
205183
assert gzip.decompress(out_file.read_bytes()) == DATA
206184
assert err == b''
207185
assert out == b''
@@ -213,12 +191,9 @@ def test_compress_infile_out_file_force(tmp_path, capsysbinary):
213191
out_file = tmp_path / "compressed.gz"
214192
out_file.touch()
215193
args = ['-f', '-o', str(out_file), str(test)]
216-
if PYPY:
217-
out, err = run_isal_igzip(*args)
218-
else:
219-
sys.argv = ['', *args]
220-
igzip.main()
221-
out, err = capsysbinary.readouterr()
194+
sys.argv = ['', *args]
195+
igzip.main()
196+
out, err = capsysbinary.readouterr()
222197
assert gzip.decompress(out_file.read_bytes()) == DATA
223198
assert err == b''
224199
assert out == b''
@@ -261,14 +236,11 @@ def test_compress_infile_out_file_inmplicit_name_prompt_accept(
261236
test.write_bytes(DATA)
262237
out_file = tmp_path / "test.gz"
263238
out_file.touch()
264-
if PYPY:
265-
out, err = run_isal_igzip(str(test), stdin=b"y\n")
266-
else:
267-
sys.argv = ['', str(test)]
268-
mock_stdin = io.BytesIO(b"y")
269-
sys.stdin = io.TextIOWrapper(mock_stdin)
270-
igzip.main()
271-
out, err = capsysbinary.readouterr()
239+
sys.argv = ['', str(test)]
240+
mock_stdin = io.BytesIO(b"y")
241+
sys.stdin = io.TextIOWrapper(mock_stdin)
242+
igzip.main()
243+
out, err = capsysbinary.readouterr()
272244
assert b"already exists; do you wish to overwrite" in out
273245
assert err == b""
274246
assert gzip.decompress(out_file.read_bytes()) == DATA
@@ -278,13 +250,9 @@ def test_compress_infile_out_file_no_name(tmp_path, capsysbinary):
278250
test = tmp_path / "test"
279251
test.write_bytes(DATA)
280252
out_file = tmp_path / "compressed.gz"
281-
args = ['-n', '-o', str(out_file), str(test)]
282-
if PYPY:
283-
out, err = run_isal_igzip(*args)
284-
else:
285-
sys.argv = ['', '-n', '-o', str(out_file), str(test)]
286-
igzip.main()
287-
out, err = capsysbinary.readouterr()
253+
sys.argv = ['', '-n', '-o', str(out_file), str(test)]
254+
igzip.main()
255+
out, err = capsysbinary.readouterr()
288256
output = out_file.read_bytes()
289257
assert gzip.decompress(output) == DATA
290258
assert err == b''

tests/test_igzip_threaded.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,54 @@ def test_threaded_write_error(monkeypatch):
8181
with igzip_threaded.open(tmp, "wb", compresslevel=3) as writer:
8282
writer.write(b"x")
8383
error.match("no attribute 'compressobj'")
84+
85+
86+
def test_close_reader():
87+
tmp = io.BytesIO(Path(TEST_FILE).read_bytes())
88+
f = igzip_threaded._ThreadedGzipReader(tmp, "rb")
89+
f.close()
90+
assert f.closed
91+
# Make sure double closing does not raise errors
92+
f.close()
93+
94+
95+
def test_close_writer():
96+
f = igzip_threaded._ThreadedGzipWriter(io.BytesIO())
97+
f.close()
98+
assert f.closed
99+
# Make sure double closing does not raise errors
100+
f.close()
101+
102+
103+
def test_reader_not_writable():
104+
with igzip_threaded.open(TEST_FILE, "rb") as f:
105+
assert not f.writable()
106+
107+
108+
def test_writer_not_readable():
109+
with igzip_threaded.open(io.BytesIO(), "wb") as f:
110+
assert not f.readable()
111+
112+
113+
def test_writer_wrong_level():
114+
with pytest.raises(ValueError) as error:
115+
igzip_threaded._ThreadedGzipWriter(io.BytesIO(), level=42)
116+
error.match("Invalid compression level")
117+
error.match("42")
118+
119+
120+
def test_reader_read_after_close():
121+
with open(TEST_FILE, "rb") as test_f:
122+
f = igzip_threaded._ThreadedGzipReader(test_f)
123+
f.close()
124+
with pytest.raises(ValueError) as error:
125+
f.read(1024)
126+
error.match("closed")
127+
128+
129+
def test_writer_write_after_close():
130+
f = igzip_threaded._ThreadedGzipWriter(io.BytesIO())
131+
f.close()
132+
with pytest.raises(ValueError) as error:
133+
f.write(b"abc")
134+
error.match("closed")

tox.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ deps=pytest
1313
passenv=
1414
PYTHON_ISAL_LINK_DYNAMIC
1515
INCLUDE
16+
setenv =
17+
PYTHONDEVMODE=1
1618
commands =
1719
# Create HTML coverage report for humans and xml coverage report for external services.
1820
coverage run --branch --source=isal -m pytest tests

0 commit comments

Comments
 (0)