Repository: pauldmccarthy/indexed_gzip (language: python)

Issue:
Support file-like objects that don't have a fileno
Support file-like objects that don't have a fileno (such as `BytesIO` streams).

If the file object passed into IndexedGzipFile implements the .fileno() method, it will pass the file descriptor to zran.c and relinquish the GIL (which is the behavior of the existing library).

Only if the file object does not implement .fileno() will it directly pass the file-like object to zran.c and hold the GIL. In that case, zran.c will directly call the `.read`, `.seek`, etc. Python methods on the file-like object instead of calling `fread`, etc. on the file descriptor.
Segmentation fault when trying to construct IndexedGzipFile with wrong argument
Consider this accident on my part:

```python3
import indexed_gzip
with open( "foo.gz", 'rb' ) as fobj:
    indexed_gzip.IndexedGzipFile( fobj )
```

This will segfault! The problem was how I specified the argument. Changing it to `IndexedGzipFile( fileobj = fobj )` fixes the problem. However, I think it still should not crash even if wrongly used. It would be nice if an exception was thrown instead. Especially as I could see this happen more often because the [gzip module](https://docs.python.org/3/library/gzip.html#gzip.open) allows the first argument to be both a name or a file object:

> The filename argument can be an actual filename (a str or bytes object), or an existing file object to read from or write to.

Btw, it might be useful to offer a `indexed_gzip.open()` method like most file modules do. Honestly, I also don't have such a method yet for `indexed_bzip2` but it's on my todo list.

The backtrace may be missing many debug symbols but I think the `fclose` being called with a nullptr file pointer should give a good enough hint:

```pascal
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7e33f5b in _IO_new_fclose (fp=0x0) at iofclose.c:48
48	iofclose.c: No such file or directory.
(gdb) bt
#0  0x00007ffff7e33f5b in _IO_new_fclose (fp=0x0) at iofclose.c:48
#1  0x00007ffff6b03e23 in ?? () from /home/hypatia/.local/lib/python3.8/site-packages/indexed_gzip/indexed_gzip.cpython-38-x86_64-linux-gnu.so
#2  0x00007ffff6afd645 in ?? () from /home/hypatia/.local/lib/python3.8/site-packages/indexed_gzip/indexed_gzip.cpython-38-x86_64-linux-gnu.so
#3  0x00000000004f4d13 in ?? ()
#4  0x00000000005c4d80 in ?? ()
#5  0x000000000056b26e in _PyEval_EvalFrameDefault ()
#6  0x00007ffff6afcc70 in ?? () from /home/hypatia/.local/lib/python3.8/site-packages/indexed_gzip/indexed_gzip.cpython-38-x86_64-linux-gnu.so
#7  0x00007ffff6afe829 in ?? () from /home/hypatia/.local/lib/python3.8/site-packages/indexed_gzip/indexed_gzip.cpython-38-x86_64-linux-gnu.so
#8  0x00007ffff6afe91f in ?? () from /home/hypatia/.local/lib/python3.8/site-packages/indexed_gzip/indexed_gzip.cpython-38-x86_64-linux-gnu.so
#9  0x00007ffff6b10bd7 in ?? () from /home/hypatia/.local/lib/python3.8/site-packages/indexed_gzip/indexed_gzip.cpython-38-x86_64-linux-gnu.so
#10 0x00000000005a7bf7 in ?? ()
#11 0x00007ffff6b0c2ff in ?? () from /home/hypatia/.local/lib/python3.8/site-packages/indexed_gzip/indexed_gzip.cpython-38-x86_64-linux-gnu.so
#12 0x00000000005f46d6 in _PyObject_MakeTpCall ()
#13 0x000000000050a978 in ?? ()
#14 0x00000000005f3d42 in PyObject_Call ()
#15 0x000000000059cb04 in ?? ()
#16 0x00000000005f463f in _PyObject_MakeTpCall ()
#17 0x0000000000570936 in _PyEval_EvalFrameDefault ()
#18 0x00000000005f7146 in _PyFunction_Vectorcall ()
#19 0x0000000000570286 in _PyEval_EvalFrameDefault ()
#20 0x00000000005f7146 in _PyFunction_Vectorcall ()
#21 0x0000000000570286 in _PyEval_EvalFrameDefault ()
#22 0x000000000056955a in _PyEval_EvalCodeWithName ()
#23 0x00000000005f7323 in _PyFunction_Vectorcall ()
#24 0x000000000056c451 in _PyEval_EvalFrameDefault ()
#25 0x00000000005f7146 in _PyFunction_Vectorcall ()
#26 0x000000000059d0fe in ?? ()
#27 0x00000000005f46d6 in _PyObject_MakeTpCall ()
#28 0x00000000005704b9 in _PyEval_EvalFrameDefault ()
#29 0x000000000056955a in _PyEval_EvalCodeWithName ()
#30 0x00000000005f7323 in _PyFunction_Vectorcall ()
#31 0x000000000056b26e in _PyEval_EvalFrameDefault ()
#32 0x000000000056955a in _PyEval_EvalCodeWithName ()
#33 0x00000000005f7323 in _PyFunction_Vectorcall ()
--Type <RET> for more, q to quit, c to continue without paging--
#34 0x000000000056b26e in _PyEval_EvalFrameDefault ()
#35 0x000000000056955a in _PyEval_EvalCodeWithName ()
#36 0x00000000005f7323 in _PyFunction_Vectorcall ()
#37 0x000000000056b26e in _PyEval_EvalFrameDefault ()
#38 0x000000000056955a in _PyEval_EvalCodeWithName ()
#39 0x000000000068c4a7 in PyEval_EvalCode ()
#40 0x000000000067bc91 in ?? ()
#41 0x000000000067bd0f in ?? ()
#42 0x000000000067bdcb in PyRun_FileExFlags ()
#43 0x000000000067de4e in PyRun_SimpleFileExFlags ()
#44 0x00000000006b6032 in Py_RunMain ()
#45 0x00000000006b63bd in Py_BytesMain ()
#46 0x00007ffff7dd60b3 in __libc_start_main (main=0x4eea30 <main>, argc=3, argv=0x7fffffffd958, init=<optimized out>, fini=<optimized out>, 
    rtld_fini=<optimized out>, stack_end=0x7fffffffd948) at ../csu/libc-start.c:308
#47 0x00000000005fa4de in _start ()
```


Discussion / hints:
This looks like a really good start! In fact, once it's cleaned up a bit, this may be all that needs to be done at the C level. 

I was wondering about your specific use case (feeding in compresed data from a stream), and had originally thought that the logic within `zran.c` may need to be adjusted to handle being fed small chunks of compressed data. 

But, thinking about it a bit more, it's probably much easier to leave `zran.c` as-is (i.e. let it think that it has a reference to the full compressed file), and implement a Python file-like object which presents itself as a full file, but maintains its own internal buffer, and handles retrieval of the compressed data on an as-needed basis on calls to `read()`. I'm not particularly experienced with networking in Python, but it sounds like the sort of thing that might already exist. Perhaps you've already got some ideas on how to tackle this?
> implement a Python file-like object which presents itself as a full file, but maintains its own internal buffer, and handles retrieval of the compressed data on an as-needed basis on calls to `read()`

Hmm, I don't think that's possible, because `zran.c` currently depends on file descriptors. I'm not aware of any way to create a Python file-like object that can "mock" the implementation of a file descriptor (which would be required to "present itself as a full file").
> Hmm, I don't think that's possible, because zran.c currently depends on file descriptors.

But in this very PR, you're proposing changes to `zran.c` which will remove that dependency :) With these changes, `zran.c` should work with any file-like, regardless of whether it is associated with a file descriptor or not. 

If there is no buffered/networked file-like implementation in the standard library, it would be easy to write one. For example, a skeleton of a primitive unbuffered implementation could look like:

```
class RemoteFile:
    def __init__(self, length):
        # length == total size of remote file
        self.__length   = length
        self.__position = 0

    def seek(self, offset, whence=SEEK_SET):
        # update __position, bounds check on __length

    def tell(self):
        return self.__position

    def read(self, nbytes):
        # download and return data from
        # __position to (__position+nbytes)
Aah, maybe when I said *it's probably much easier to leave zran.c as-is*, you took it to mean leaving `zran.c` in its current state, and disregarding this PR - this is not what I meant. Rather, I was suggesting that your current changes could be added to `zran.c`, but then the rest of the implementation could be done in python/cython.
Ah, I see what you mean! In my case, I am already using another library ([Apache Beam](https://beam.apache.org/releases/pydoc/2.26.0/apache_beam.io.azure.blobstorageio.html#apache_beam.io.azure.blobstorageio.BlobStorageIO.open)) for the file-like object wrapper abstraction.
Hi @epicfaace, just a note that I will get around to this - I presume your forked release is working for you for the time being?

Relinquishing the GIL is kind of important for my use-case - I have a GUI application in which I use `indexed_gzip` to decompress data in separate threads. So what I might do is take what you've done in this PR, add the `FILE *` pointer back in, and build in some sort of toggle mechanism, allowing either a `FILE *` or a `PyObject *` to be used (and relinquishing the GIL only on the former). I'm aiming to get around to this soon!
@pauldmccarthy Yes, that's correct -- I'm using my forked version at the moment. I'd like to switch to using this upstream library as a dependency as soon as practicable, though, so how about this: when you start having time to implement the `FILE *` / `PyObject *`, just let me know, otherwise, if I have the bandwidth to begin doing this earlier than you do, I'll let you know and work on it directly in this PR.
Okay, I have time to work on this now.
Okay, @pauldmccarthy , this is passing CI now and should be all ready to go!
Hey @pauldmccarthy , just a gentle reminder to take a look whenever you get a chance. Let me know if I can help in any way by explaining anything / changing anything in this PR.
No problem - thanks for your work! Nothing major stood out on a quick scan of the diff, but I'll take a closer look later this week...
@pauldmccarthy this should be ready for another pass now. Note the substantial change in the EOF logic.
Thanks a lot for your work on this, and thanks for your patience. I've had work stuff come up, but will aim to get this reviewed and merged in before the end of the weekend..
Thank _you_ for taking the time to review this -- it's open source software, the real burden is on the maintainer, not the contributor!
LGTM - thanks so much for your effort! I'll follow this up with a quick clean-up, and will aim to release a new version next week - no breaking changes, so I guess that will make it version 1.5.0.
This is a reasonable suggestion - I've tried to make the `IndexedGzipFile` interface compatible with that of `GzipFile`, so I'm not sure why I didn't allow the first argument to be either a file object or a path..
Actually, I've just discovered that `gzip.GzipFile` does *not* allow you to pass either a file name or a file object:

```
In [3]: import gzip

In [4]: g1 = gzip.GzipFile('test.gz')

In [5]: g2 = gzip.GzipFile(open('test.gz'))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-08fd1f6d2f15> in <module>
----> 1 g2 = gzip.GzipFile(open('ctest_zran_15258347_False.gz'))

/usr/lib/python3.6/gzip.py in __init__(self, filename, mode, compresslevel, fileobj, mtime)
    161             mode += 'b'
    162         if fileobj is None:
--> 163             fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
    164         if filename is None:
    165             filename = getattr(fileobj, 'name', '')

TypeError: expected str, bytes or os.PathLike object, not _io.TextIOWrapper
```

It's only the `gzip.open` function which has this behaviour. Oh well, I've already implemented it for `IndexedGzipFile`...

Fix patch:
diff --git a/AUTHOR b/AUTHOR
new file mode 100644
index 00000000..becefeab
--- /dev/null
+++ b/AUTHOR
@@ -0,0 +1,8 @@
+Paul McCarthy (@pauldmccarthy)
+Zalan Rajna (@zrajna)
+Martin Craig (@mcraig-ibme)
+Chris Markiewicz (@effigies)
+Omer Ozarslan (@ozars)
+@DarioDaF
+Sławomir Zborowski (@szborows)
+Ashwin Ramaswami (@epicfaace)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 845e8f6d..7774f0a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
 # `indexed_gzip` changelog
 
 
+## 1.5.0 (March 19th 2021)
+
+
+* Added support for in-memory file-like objects (#55).
+* Fixed a bug whereby a segmentation fault could occur if an `IndexedGzipFile`
+  was created with a path to a non-existent file (#56).
+
+
 ## 1.4.0 (January 2nd 2021)
 
 
diff --git a/LICENSE b/LICENSE
index bff8a6ca..97edb6c8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-indexed_gzip: Copyright (C) 2016-2020 Paul McCarthy
+indexed_gzip: Copyright (C) 2016-2021 Paul McCarthy
 
 zlib: Copyright (C) 1995-2013 Jean-loup Gailly and Mark Adler
 
diff --git a/README.md b/README.md
index 4d6b614d..6f78331a 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,26 @@ data = myfile.read(1048576)
 ```
 
 
+## Using with in-memory data
+
+You can use `indexed_gzip` with any Python file-like object. For example:
+
+```python
+
+import io
+import indexed_gzip as igzip
+
+# Load some gzip data from somewhere
+with open('my_file.gz') as f:
+    data = f.read()
+
+# Create an IndexedGzipFile based on the
+# in-memory data buffer
+gzf = igzip.IndexedGzipFile(fileobj=io.BytesIO(data))
+uncompressed = gzf.read(1048576)
+```
+
+
 ## Using with `nibabel`
 
 
@@ -132,8 +152,8 @@ image = nib.load('big_image.nii.gz')
 ```
 
 
-If you are using `nibabel` 2.2.x, you need to explicitly set the `keep_file_open`
-flag:
+If you are using `nibabel` 2.2.x, you need to explicitly set the
+`keep_file_open` flag:
 
 
 ```python
@@ -202,10 +222,10 @@ fobj = igzip.IndexedGzipFile('big_file.gz', index_file='big_file.gzidx')
 ## Write support
 
 
-`indexed_gzip` does not currently have any support for writing. Currently if you
-wish to write to a file, you will need to save the file by alternate means (e.g.
-via `gzip` or `nibabel`), and then re-create a new `IndexedGzipFile` instance.
-For example:
+`indexed_gzip` does not currently have any support for writing. Currently if
+you wish to write to a file, you will need to save the file by alternate means
+(e.g.  via `gzip` or `nibabel`), and then re-create a new `IndexedGzipFile`
+instance.  For example:
 
 
 ```python
@@ -275,16 +295,16 @@ Initial work on `indexed_gzip` took place at
 [FMRIB Centre](https://www.ndcn.ox.ac.uk/divisions/fmrib/), at the
 University of Oxford, UK.
 
-
 Many thanks to the following contributors (listed chronologically):
 
- - Zalan Rajna (@zrajna): bug fixes (#2)
- - Martin Craig (@mcraig-ibme): porting `indexed_gzip` to Windows (#3)
+ - Zalan Rajna (@zrajna): Bug fixes (#2)
+ - Martin Craig (@mcraig-ibme): Porting `indexed_gzip` to Windows (#3)
  - Chris Markiewicz (@effigies): Option to drop file handles (#6)
  - Omer Ozarslan (@ozars): Index import/export (#8)
  - @DarioDaF: Windows overflow bug (#30)
  - Sławomir Zborowski (@szborows): `seek_points` method (#35), README fixes
    (#34)
+ - Ashwin Ramaswami (@epicfaace): Support for in-memory file objects (#55)
 
 
 ## License
diff --git a/indexed_gzip/__init__.py b/indexed_gzip/__init__.py
index 23efe996..fe79c91a 100644
--- a/indexed_gzip/__init__.py
+++ b/indexed_gzip/__init__.py
@@ -2,8 +2,6 @@
 #
 # __init__.py - The indexed_gzip namespace.
 #
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
 """The indexed_gzip namespace. """
 
 
@@ -21,4 +19,4 @@
 """
 
 
-__version__ = '1.4.0'
+__version__ = '1.5.0'
diff --git a/indexed_gzip/indexed_gzip.pyx b/indexed_gzip/indexed_gzip.pyx
index 112d908a..2adb638f 100644
--- a/indexed_gzip/indexed_gzip.pyx
+++ b/indexed_gzip/indexed_gzip.pyx
@@ -2,8 +2,6 @@
 #
 # The IndexedGzipFile class.
 #
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
 
 """This module provides the :class:`IndexedGzipFile` class, a drop-in
 replacement for the built-in ``gzip.GzipFile`` class, for faster read-only
@@ -37,14 +35,14 @@ from cpython.ref cimport PyObject
 
 cimport indexed_gzip.zran as zran
 
-import io
-import os
-import pickle
-import logging
-import tempfile
-import warnings
-import threading
-import contextlib
+import            io
+import            os
+import os.path as op
+import            pickle
+import            logging
+import            warnings
+import            threading
+import            contextlib
 
 
 builtin_open = open
@@ -180,19 +178,9 @@ class IndexedGzipFile(io.BufferedReader):
             index = None
 
         else:
-            # zran.c:zran_export_index requires a file
-            # descriptor, so we give it a tempoorary
-            # file, and then read the bytes into memory.
-            tmpfile = None
-            try:
-                tmpfile = tempfile.NamedTemporaryFile(delete=False)
-                tmpfile.close()
-                self.export_index(tmpfile.name)
-                with builtin_open(tmpfile.name, 'rb') as f:
-                    index = f.read()
-            finally:
-                if tmpfile is not None:
-                    os.remove(tmpfile.name)
+            index = io.BytesIO()
+            self.export_index(fileobj=index)
+            index = index.getvalue()
 
         state = {
             'filename'         : fobj.filename,
@@ -346,6 +334,14 @@ cdef class _IndexedGzipFile:
             fileobj  = filename
             filename = None
 
+        # If __file_handle is called on a file
+        # that doesn't exist, it passes the
+        # path directly to fopen, which causes
+        # a segmentation faullt on linux. So
+        # let's check before that happens.
+        if (filename is not None) and (not op.isfile(filename)):
+            raise ValueError('File {} does not exist'.format(filename))
+
         mode     = 'rb'
         own_file = fileobj is None
 
@@ -365,7 +361,6 @@ cdef class _IndexedGzipFile:
             except io.UnsupportedOperation:
                 fd  = NULL
 
-
         self.spacing          = spacing
         self.window_size      = window_size
         self.readbuf_size     = readbuf_size
@@ -375,16 +370,18 @@ cdef class _IndexedGzipFile:
         self.filename         = filename
         self.own_file         = own_file
         self.pyfid            = fileobj
-        self.index.fd         = fd
-        self.index.f          = <PyObject*>fileobj
 
         if self.auto_build: flags = zran.ZRAN_AUTO_BUILD
         else:               flags = 0
 
+        # Set index.fd here just for the initial
+        # call, as __file_handle may otherwise
+        # manipulate it incorrectly
+        self.index.fd = fd
         with self.__file_handle():
             if zran.zran_init(index=&self.index,
                               fd=self.index.fd,
-                              f=<PyObject*>self.index.f if self.index.fd == NULL else NULL,
+                              f=<PyObject*>fileobj,
                               spacing=spacing,
                               window_size=window_size,
                               readbuf_size=readbuf_size,
@@ -425,7 +422,7 @@ cdef class _IndexedGzipFile:
             # context manager reentrant.
             if self.index.fd is not NULL:
                 yield
-            
+
             # If a file-like object exists (without an associated
             # file descriptor, since self.index.fd is NULL),
             # also return it.
@@ -435,7 +432,6 @@ cdef class _IndexedGzipFile:
             # otherwise we open a new
             # file handle on each access
             else:
-
                 try:
                     self.index.fd = fopen(self.filename.encode(), 'rb')
                     yield
@@ -507,7 +503,8 @@ cdef class _IndexedGzipFile:
         self.pyfid     = None
         self.finalized = True
 
-        log.debug('%s.close()', type(self).__name__)
+        if log is not None:
+            log.debug('%s.close()', type(self).__name__)
 
 
     @property
@@ -602,12 +599,8 @@ cdef class _IndexedGzipFile:
         if whence not in (SEEK_SET, SEEK_CUR, SEEK_END):
             raise ValueError('Invalid value for whence: {}'.format(whence))
 
-        if index.fd == NULL:
-            with self.__file_handle():
-                ret = zran.zran_seek(index, off, c_whence, NULL)
-        else:
-            with self.__file_handle(), nogil:
-                ret = zran.zran_seek(index, off, c_whence, NULL)
+        with self.__file_handle(), nogil:
+            ret = zran.zran_seek(index, off, c_whence, NULL)
 
         if ret < 0:
             raise ZranError('zran_seek returned error: {}'.format(ret))
@@ -661,21 +654,20 @@ cdef class _IndexedGzipFile:
                 # buffer location
                 buffer = <char *>buf.buffer + offset
 
-                if index.fd == NULL:
+                with nogil:
                     ret = zran.zran_read(index, buffer, bufsz)
-                else:
-                    with nogil:
-                        ret = zran.zran_read(index, buffer, bufsz)
 
                 # see how the read went
                 if ret == zran.ZRAN_READ_FAIL:
-                    raise ZranError('zran_read returned error ({})'.format(ret))
+                    raise ZranError('zran_read returned error '
+                                    '({})'.format(ret))
 
                 # This will happen if the current
                 # seek point is not covered by the
                 # index, and auto-build is disabled
                 elif ret == zran.ZRAN_READ_NOT_COVERED:
-                    raise NotCoveredError('Index does not cover current offset')
+                    raise NotCoveredError('Index does not cover '
+                                          'current offset')
 
                 # No bytes were read, and there are
                 # no more bytes to read. This will
@@ -730,12 +722,8 @@ cdef class _IndexedGzipFile:
         try:
 
             vbuf = <void *>pbuf.buf
-            if index.fd == NULL:
-                with self.__file_handle():
-                    ret = zran.zran_read(index, vbuf, bufsz)
-            else:
-                with self.__file_handle(), nogil:
-                    ret = zran.zran_read(index, vbuf, bufsz)
+            with self.__file_handle(), nogil:
+                ret = zran.zran_read(index, vbuf, bufsz)
 
         # release the py_buffer
         finally:
@@ -907,11 +895,14 @@ cdef class _IndexedGzipFile:
                     'File should be opened in writeable binary mode.')
 
         try:
+            # Pass both the Python file object and
+            # file descriptor (if this is an actual
+            # file) to the zran_export_index function
             try:
-                fd  = fdopen(fileobj.fileno(), 'wb')
+                fd = fdopen(fileobj.fileno(), 'wb')
             except io.UnsupportedOperation:
-                fd  = NULL
-            ret = zran.zran_export_index(&self.index, fd, <PyObject*>fileobj if fd == NULL else NULL)
+                fd = NULL
+            ret = zran.zran_export_index(&self.index, fd, <PyObject*>fileobj)
             if ret != zran.ZRAN_EXPORT_OK:
                 raise ZranError('export_index returned error: {}'.format(ret))
 
@@ -952,11 +943,14 @@ cdef class _IndexedGzipFile:
                     'File should be opened read-only binary mode.')
 
         try:
+            # Pass both the Python file object and
+            # file descriptor (if this is an actual
+            # file) to the zran_import_index function
             try:
-                fd  = fdopen(fileobj.fileno(), 'rb')
+                fd = fdopen(fileobj.fileno(), 'rb')
             except io.UnsupportedOperation:
-                fd  = NULL
-            ret = zran.zran_import_index(&self.index, fd, <PyObject*>fileobj if fd == NULL else NULL)
+                fd = NULL
+            ret = zran.zran_import_index(&self.index, fd, <PyObject*>fileobj)
             if ret != zran.ZRAN_IMPORT_OK:
                 raise ZranError('import_index returned error: {}'.format(ret))
 
@@ -1034,20 +1028,8 @@ def unpickle(state):
     gzobj = IndexedGzipFile(**state)
 
     if index is not None:
-        tmpfile = None
-        try:
-            # zran.c:zran_import_index requires an
-            # actual file with a file descriptor,
-            # so we write the index data out to a
-            # temp file, and then pass the file in.
-            tmpfile = tempfile.NamedTemporaryFile(delete=False)
-            tmpfile.write(index)
-            tmpfile.close()
-            gzobj.import_index(tmpfile.name)
+        gzobj.import_index(fileobj=io.BytesIO(index))
 
-        finally:
-            if tmpfile is not None:
-                os.remove(tmpfile.name)
     gzobj.seek(tell)
 
     return gzobj
diff --git a/indexed_gzip/zran.c b/indexed_gzip/zran.c
index dd0fc883..b2c38b54 100644
--- a/indexed_gzip/zran.c
+++ b/indexed_gzip/zran.c
@@ -5,8 +5,6 @@
  *
  * This module was originally based on the zran example, written by Mark
  * Alder, which ships with the zlib source code.
- *
- * Author: Paul McCarthy <pauldmccarthy@gmail.com>
  */
 
 #include <math.h>
@@ -34,10 +32,12 @@ static int is_readonly(FILE *fd, PyObject *f)
 /* Check if file is read-only */
 static int is_readonly(FILE *fd, PyObject *f)
 {
-    return fd != NULL ? (fcntl(fileno(fd), F_GETFL) & O_ACCMODE) == O_RDONLY : 1;
+    /* Skip the test for python file-likes */
+    return fd != NULL
+      ? (fcntl(fileno(fd), F_GETFL) & O_ACCMODE) == O_RDONLY
+      : 1;
 }
 
-
 static uint32_t max(uint32_t a, uint32_t b) {
 
   if (a > b) return a;
@@ -45,9 +45,11 @@ static uint32_t max(uint32_t a, uint32_t b) {
 }
 #endif
 
+
 #include "zran.h"
 #include "zran_file_util.h"
 
+
 #ifdef NO_C99
 static double round(double val)
 {
@@ -110,8 +112,8 @@ static int _zran_free_unused(
  */
 static uint64_t _zran_index_limit(
   zran_index_t *index,      /* The index */
-  uint8_t       compressed  /* Pass in non-0 to get the compressed stream limit,
-                               or 0 for the uncompressed limit. */
+  uint8_t       compressed  /* Pass in non-0 to get the compressed stream
+                               limit, or 0 for the uncompressed limit. */
 );
 
 
@@ -918,10 +920,12 @@ uint64_t _zran_estimate_offset(
      * between the compressed/uncompressed data streams.
      */
     else if (compressed) {
-        estimate = round(offset * ((float)last->uncmp_offset / last->cmp_offset));
+        estimate = round(offset *
+                         ((float)last->uncmp_offset / last->cmp_offset));
     }
     else {
-        estimate = round(offset * ((float)last->cmp_offset / last->uncmp_offset));
+        estimate = round(offset *
+                         ((float)last->cmp_offset / last->uncmp_offset));
     }
 
 
@@ -946,7 +950,6 @@ int _zran_add_point(zran_index_t  *index,
     zran_point_t *next       = NULL;
 
     #ifdef ZRAN_VERBOSE
-
     zran_log("_zran_add_point(%i, c=%lld + %i, u=%lld, data=%u / %u)\n",
              index->npoints,
              cmp_offset,
@@ -1005,7 +1008,9 @@ int _zran_add_point(zran_index_t  *index,
     if (uncmp_offset > 0) {
         if (data_offset >= index->window_size) {
 
-            memcpy(point_data, data + (data_offset - index->window_size), index->window_size);
+            memcpy(point_data,
+                   data + (data_offset - index->window_size),
+                   index->window_size);
 
             zran_log("Copy %u bytes from %u to %u\n",
                      index->window_size,
@@ -1031,7 +1036,6 @@ int _zran_add_point(zran_index_t  *index,
         }
     }
 
-
     index->npoints++;
 
     return 0;
@@ -2149,6 +2153,7 @@ not_covered:     return ZRAN_SEEK_NOT_COVERED;
     return ZRAN_SEEK_EOF;
 }
 
+
 /* Return the current seek position in the uncompressed data stream. */
 uint64_t zran_tell(zran_index_t *index) {
 
@@ -2466,8 +2471,8 @@ eof:         return ZRAN_READ_EOF;
  * binary write mode.
  */
 int zran_export_index(zran_index_t *index,
-                      FILE *fd,
-                      PyObject *f) {
+                      FILE         *fd,
+                      PyObject     *f) {
 
     /*
      * TODO: Endianness check for fwrite calls. Prefer little-endian to be
@@ -2492,39 +2497,39 @@ int zran_export_index(zran_index_t *index,
     f_ret = fwrite_(zran_magic_bytes, sizeof(zran_magic_bytes), 1, fd, f);
 
     if (ferror_(fd, f)) goto fail;
-    if (f_ret != 1) goto fail;
+    if (f_ret != 1)     goto fail;
 
     /* Write compressed size, and check for errors. */
     f_ret = fwrite_(&index->compressed_size,
                    sizeof(index->compressed_size), 1, fd, f);
 
     if (ferror_(fd, f)) goto fail;
-    if (f_ret != 1) goto fail;
+    if (f_ret != 1)     goto fail;
 
     /* Write uncompressed size, and check for errors. */
     f_ret = fwrite_(&index->uncompressed_size,
                    sizeof(index->uncompressed_size), 1, fd, f);
 
     if (ferror_(fd, f)) goto fail;
-    if (f_ret != 1) goto fail;
+    if (f_ret != 1)     goto fail;
 
     /* Write spacing, and check for errors. */
     f_ret = fwrite_(&index->spacing, sizeof(index->spacing), 1, fd, f);
 
     if (ferror_(fd, f)) goto fail;
-    if (f_ret != 1) goto fail;
+    if (f_ret != 1)     goto fail;
 
     /* Write window size, and check for errors. */
     f_ret = fwrite_(&index->window_size, sizeof(index->window_size), 1, fd, f);
 
     if (ferror_(fd, f)) goto fail;
-    if (f_ret != 1) goto fail;
+    if (f_ret != 1)     goto fail;
 
     /* Write number of points, and check for errors. */
     f_ret = fwrite_(&index->npoints, sizeof(index->npoints), 1, fd, f);
 
     if (ferror_(fd, f)) goto fail;
-    if (f_ret != 1) goto fail;
+    if (f_ret != 1)     goto fail;
 
     /*
      * We will make two passes over points list now. In the first pass, offset
@@ -2549,22 +2554,24 @@ int zran_export_index(zran_index_t *index,
          */
 
         /* Write compressed offset, and check for errors. */
-        f_ret = fwrite_(&point->cmp_offset, sizeof(point->cmp_offset), 1, fd, f);
+        f_ret = fwrite_(&point->cmp_offset,
+                        sizeof(point->cmp_offset), 1, fd, f);
 
         if (ferror_(fd, f)) goto fail;
-        if (f_ret != 1) goto fail;
+        if (f_ret != 1)     goto fail;
 
         /* Write uncompressed offset, and check for errors. */
-        f_ret = fwrite_(&point->uncmp_offset, sizeof(point->uncmp_offset), 1, fd, f);
+        f_ret = fwrite_(&point->uncmp_offset,
+                        sizeof(point->uncmp_offset), 1, fd, f);
 
         if (ferror_(fd, f)) goto fail;
-        if (f_ret != 1) goto fail;
+        if (f_ret != 1)     goto fail;
 
         /* Write bit offset, and check for errors. */
         f_ret = fwrite_(&point->bits, sizeof(point->bits), 1, fd, f);
 
         if (ferror_(fd, f)) goto fail;
-        if (f_ret != 1) goto fail;
+        if (f_ret != 1)     goto fail;
 
         zran_log("zran_export_index: (%lu, %lu, %lu, %u)\n",
                  (index->npoints - (list_end - point)), // point index
@@ -2590,7 +2597,7 @@ int zran_export_index(zran_index_t *index,
         f_ret = fwrite_(point->data, index->window_size, 1, fd, f);
 
         if (ferror_(fd, f)) goto fail;
-        if (f_ret != 1) goto fail;
+        if (f_ret != 1)     goto fail;
 
         /* Print first and last three bytes of the checkpoint window. */
         zran_log("zran_export_index: "
@@ -2616,13 +2623,12 @@ int zran_export_index(zran_index_t *index,
     f_ret = fflush_(fd, f);
 
     if (ferror_(fd, f)) goto fail;
-    if (f_ret != 0) goto fail;
+    if (f_ret != 0)     goto fail;
 
     return ZRAN_EXPORT_OK;
 
 fail:
     return ZRAN_EXPORT_WRITE_ERROR;
-
 }
 
 /*
@@ -2630,8 +2636,8 @@ int zran_export_index(zran_index_t *index,
  * binary read mode.
  */
 int zran_import_index(zran_index_t *index,
-                      FILE *fd,
-                      PyObject *f) {
+                      FILE         *fd,
+                      PyObject     *f) {
 
     /* Used for checking return value of fread calls. */
     size_t f_ret;
@@ -2664,10 +2670,9 @@ int zran_import_index(zran_index_t *index,
     /* Read magic bytes, and check for file errors and EOF. */
     f_ret = fread_(magic_bytes, sizeof(magic_bytes), 1, fd, f);
 
-    if (feof_(fd, f, f_ret))   goto eof;
-    
-    if (ferror_(fd, f)) goto read_error;
-    if (f_ret != 1) goto read_error;
+    if (feof_(fd, f, f_ret)) goto eof;
+    if (ferror_(fd, f))      goto read_error;
+    if (f_ret != 1)          goto read_error;
 
     /* Verify magic bytes. */
     if (memcmp(magic_bytes, zran_magic_bytes, sizeof(magic_bytes))) {
@@ -2678,7 +2683,7 @@ int zran_import_index(zran_index_t *index,
     f_ret = fread_(&compressed_size, sizeof(compressed_size), 1, fd, f);
 
     if (ferror_(fd, f)) goto read_error;
-    if (f_ret != 1) goto read_error;
+    if (f_ret != 1)     goto read_error;
 
     /*
      * Compare compressed_size in the index file  to the existing size in
@@ -2686,15 +2691,15 @@ int zran_import_index(zran_index_t *index,
      * this index file is not created for this compressed file.
      */
     if (compressed_size != index->compressed_size) goto inconsistent;
-    
-    if (feof_(fd, f, f_ret))   goto eof;
+
+    if (feof_(fd, f, f_ret)) goto eof;
 
     /* Read uncompressed size, and check for file errors and EOF. */
     f_ret = fread_(&uncompressed_size, sizeof(uncompressed_size), 1, fd, f);
 
-    if (feof_(fd, f, f_ret))   goto eof;
-    if (ferror_(fd, f)) goto read_error;
-    if (f_ret != 1) goto read_error;
+    if (feof_(fd, f, f_ret)) goto eof;
+    if (ferror_(fd, f))      goto read_error;
+    if (f_ret != 1)          goto read_error;
 
     /*
      * Uncompressed size may not be set in either current index or exported
@@ -2707,16 +2712,16 @@ int zran_import_index(zran_index_t *index,
     /* Read spacing, and check for file errors and EOF. */
     f_ret = fread_(&spacing, sizeof(spacing), 1, fd, f);
 
-    if (feof_(fd, f, f_ret))   goto eof;
-    if (ferror_(fd, f)) goto read_error;
-    if (f_ret != 1) goto read_error;
+    if (feof_(fd, f, f_ret)) goto eof;
+    if (ferror_(fd, f))      goto read_error;
+    if (f_ret != 1)          goto read_error;
 
     /* Read window size, and check for file errors and EOF. */
     f_ret = fread_(&window_size, sizeof(window_size), 1, fd, f);
 
-    if (feof_(fd, f, f_ret))   goto eof;
-    if (ferror_(fd, f)) goto read_error;
-    if (f_ret != 1) goto read_error;
+    if (feof_(fd, f, f_ret)) goto eof;
+    if (ferror_(fd, f))      goto read_error;
+    if (f_ret != 1)          goto read_error;
 
     /*
      * Make sanity checks for window size and spacing. These are similar to
@@ -2728,9 +2733,9 @@ int zran_import_index(zran_index_t *index,
     /* Read number of points, and check for file errors and EOF. */
     f_ret = fread_(&npoints, sizeof(npoints), 1, fd, f);
 
-    if (feof_(fd, f, f_ret))   goto eof;
-    if (ferror_(fd, f)) goto read_error;
-    if (f_ret != 1) goto read_error;
+    if (feof_(fd, f, f_ret)) goto eof;
+    if (ferror_(fd, f))      goto read_error;
+    if (f_ret != 1)          goto read_error;
 
     zran_log("zran_import_index: (%lu, %lu, %u, %u, %u)\n",
              compressed_size,
@@ -2763,25 +2768,27 @@ int zran_import_index(zran_index_t *index,
     {
 
         /* Read compressed offset, and check for errors. */
-        f_ret = fread_(&point->cmp_offset, sizeof(point->cmp_offset), 1, fd, f);
+        f_ret = fread_(&point->cmp_offset,
+                       sizeof(point->cmp_offset), 1, fd, f);
 
-        if (feof_(fd, f, f_ret))   goto eof;
-        if (ferror_(fd, f)) goto read_error;
-        if (f_ret != 1) goto read_error;
+        if (feof_(fd, f, f_ret)) goto eof;
+        if (ferror_(fd, f))      goto read_error;
+        if (f_ret != 1)          goto read_error;
 
         /* Read uncompressed offset, and check for errors. */
-        f_ret = fread_(&point->uncmp_offset, sizeof(point->uncmp_offset), 1, fd, f);
+        f_ret = fread_(&point->uncmp_offset,
+                       sizeof(point->uncmp_offset), 1, fd, f);
 
-        if (feof_(fd, f, f_ret))   goto eof;
-        if (ferror_(fd, f)) goto read_error;
-        if (f_ret != 1) goto read_error;
+        if (feof_(fd, f, f_ret)) goto eof;
+        if (ferror_(fd, f))      goto read_error;
+        if (f_ret != 1)          goto read_error;
 
         /* Read bit offset, and check for errors. */
         f_ret = fread_(&point->bits, sizeof(point->bits), 1, fd, f);
 
-        if (feof_(fd, f, f_ret))   goto eof;
-        if (ferror_(fd, f)) goto read_error;
-        if (f_ret != 1) goto read_error;
+        if (feof_(fd, f, f_ret)) goto eof;
+        if (ferror_(fd, f))      goto read_error;
+        if (f_ret != 1)          goto read_error;
 
         zran_log("zran_import_index: (%lu, %lu, %lu, %u)\n",
                  (npoints - (list_end - point)), // point index
@@ -2819,8 +2826,8 @@ int zran_import_index(zran_index_t *index,
         f_ret = fread_(point->data, window_size, 1, fd, f);
 
         if (feof_(fd, f, f_ret) && point < list_end - 1) goto eof;
-        if (ferror_(fd, f))                       goto read_error;
-        if (f_ret != 1)                       goto read_error;
+        if (ferror_(fd, f))                              goto read_error;
+        if (f_ret != 1)                                  goto read_error;
 
         /*
          * TODO: If there are still more data after importing is done, it
diff --git a/indexed_gzip/zran.h b/indexed_gzip/zran.h
index 7bff35fd..0a3905ab 100644
--- a/indexed_gzip/zran.h
+++ b/indexed_gzip/zran.h
@@ -6,8 +6,6 @@
  * Alder, which ships with the zlib source code. It allows the creation
  * of an index into a compressed file, which is used to improve the speed
  * of random seek/read access to the uncompressed data.
- *
- * Author: Paul McCarthy <pauldmccarthy@gmail.com>
  */
 
 #include <stdlib.h>
diff --git a/indexed_gzip/zran.pxd b/indexed_gzip/zran.pxd
index 0017964c..44b4722f 100644
--- a/indexed_gzip/zran.pxd
+++ b/indexed_gzip/zran.pxd
@@ -1,8 +1,6 @@
 #
 # Cython declaration for the zran library.
 #
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
 
 from libc.stdio  cimport FILE
 from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t, int64_t
diff --git a/indexed_gzip/zran_file_util.c b/indexed_gzip/zran_file_util.c
index 577c58d8..692f0e82 100644
--- a/indexed_gzip/zran_file_util.c
+++ b/indexed_gzip/zran_file_util.c
@@ -2,8 +2,8 @@
  * zran_file_util.c - file utilities used in zran.c to manipulate either
  * Python file-like objects or file descriptors.
  *
- *
- * Author: Paul McCarthy <pauldmccarthy@gmail.com>
+ * Functions which interact with Python file-likes will acquire and release
+ * the GIL as needeed.
  */
 
 #include <math.h>
@@ -27,122 +27,190 @@
 #define FTELL ftello
 #endif
 
+/*
+ * The zran functions are typically called with the GIL released. These
+ * macros are used to temporarily (re-)acquire and release the GIL when
+ * interacting with Python file-like objects.
+ */
+#define _ZRAN_FILE_UTIL_ACQUIRE_GIL \
+    PyGILState_STATE s;             \
+    s = PyGILState_Ensure();
+
+#define _ZRAN_FILE_UTIL_RELEASE_GIL \
+    PyGILState_Release(s);
+
 
 /*
- * Implements a method analogous to fread that is performed on Python file-like objects.
+ * Implements a method analogous to fread that is performed on Python
+ * file-like objects.
  */
 size_t _fread_python(void *ptr, size_t size, size_t nmemb, PyObject *f) {
-    PyObject *data;
-    char *buf;
+
+    PyObject  *data;
+    char      *buf;
     Py_ssize_t len;
+
+    _ZRAN_FILE_UTIL_ACQUIRE_GIL
+
     if ((data = PyObject_CallMethod(f, "read", "(n)", size * nmemb)) == NULL)
         goto fail;
     if ((buf = PyBytes_AsString(data)) == NULL)
         goto fail;
     if ((len = PyBytes_Size(data)) == -1)
         goto fail;
+
     memmove(ptr, buf, (size_t) len);
+
     Py_DECREF(data);
+    _ZRAN_FILE_UTIL_RELEASE_GIL
     return (size_t) len / size;
 
 fail:
     Py_XDECREF(data);
+    _ZRAN_FILE_UTIL_RELEASE_GIL
     return 0;
 }
 
 /*
- * Implements a method analogous to ftell that is performed on Python file-like objects.
+ * Implements a method analogous to ftell that is performed on Python
+ * file-like objects.
  */
 int64_t _ftell_python(PyObject *f) {
     PyObject *data;
-    int64_t result;
-    if ((data = PyObject_CallMethod(f, "tell", NULL)) == NULL)
+    int64_t   result;
+
+    _ZRAN_FILE_UTIL_ACQUIRE_GIL
+
+    data = PyObject_CallMethod(f, "tell", NULL);
+    if (data == NULL)
         goto fail;
-    if ((result = PyLong_AsLong(data)) == -1 && PyErr_Occurred())
+
+    result = PyLong_AsLong(data);
+    if (result == -1 && PyErr_Occurred())
         goto fail;
+
     Py_DECREF(data);
+    _ZRAN_FILE_UTIL_RELEASE_GIL
     return result;
 
 fail:
     Py_XDECREF(data);
+    _ZRAN_FILE_UTIL_RELEASE_GIL
     return -1;
 }
 
 /*
- * Implements a method analogous to fseek that is performed on Python file-like objects.
+ * Implements a method analogous to fseek that is performed on Python
+ * file-like objects.
  */
 int _fseek_python(PyObject *f, int64_t offset, int whence) {
+
     PyObject *data;
-    if ((data = PyObject_CallMethod(f, "seek", "(l,i)", offset, whence)) == NULL)
+
+    _ZRAN_FILE_UTIL_ACQUIRE_GIL
+
+    data = PyObject_CallMethod(f, "seek", "(l,i)", offset, whence);
+    if (data == NULL)
         goto fail;
+
     Py_DECREF(data);
+    _ZRAN_FILE_UTIL_RELEASE_GIL
     return 0;
 
 fail:
     Py_XDECREF(data);
+    _ZRAN_FILE_UTIL_RELEASE_GIL
     return -1;
 }
 
 /*
- * Implements a method analogous to feof that is performed on Python file-like objects.
- * If f_ret, the number of bytes returned by the last read, is zero, then we're at EOF.
+ * Implements a method analogous to feof that is performed on Python file-like
+ * objects.  If f_ret, the number of bytes returned by the last read, is zero,
+ * then we're at EOF.
  */
 int _feof_python(PyObject *f, size_t f_ret) {
     return f_ret == 0;
 }
 
 /*
- * Implements a method analogous to ferror that is performed on Python file-like objects.
+ * Implements a method analogous to ferror that is performed on Python
+ * file-like objects.
  */
 int _ferror_python(PyObject *f) {
-    return PyErr_Occurred() ? 1 : 0;
+    int result;
+
+    _ZRAN_FILE_UTIL_ACQUIRE_GIL
+    result = PyErr_Occurred();
+    _ZRAN_FILE_UTIL_RELEASE_GIL
+
+    if (result != NULL) return 1;
+    else                return 0;
 }
 
 /*
- * Implements a method analogous to fflush that is performed on Python file-like objects.
+ * Implements a method analogous to fflush that is performed on Python
+ * file-like objects.
  */
 int _fflush_python(PyObject *f) {
     PyObject *data;
+
+    _ZRAN_FILE_UTIL_ACQUIRE_GIL
     if ((data = PyObject_CallMethod(f, "flush", NULL)) == NULL) goto fail;
+
     Py_DECREF(data);
+    _ZRAN_FILE_UTIL_RELEASE_GIL
     return 0;
 
 fail:
     Py_XDECREF(data);
+    _ZRAN_FILE_UTIL_RELEASE_GIL
     return -1;
 }
 
 /*
- * Implements a method analogous to fwrite that is performed on Python file-like objects.
+ * Implements a method analogous to fwrite that is performed on Python
+ * file-like objects.
  */
-size_t _fwrite_python(const void *ptr, size_t size, size_t nmemb, PyObject *f) {
+size_t _fwrite_python(const void *ptr,
+                      size_t      size,
+                      size_t      nmemb,
+                      PyObject   *f) {
+
     PyObject *input;
     PyObject *data = NULL;
-    long len;
+    long      len;
+
+    _ZRAN_FILE_UTIL_ACQUIRE_GIL
     if ((input = PyBytes_FromStringAndSize(ptr, size * nmemb)) == NULL)
         goto fail;
     if ((data = PyObject_CallMethod(f, "write", "(O)", input)) == NULL)
         goto fail;
+
     #if PY_MAJOR_VERSION >= 3
     if ((len = PyLong_AsLong(data)) == -1 && PyErr_Occurred())
         goto fail;
     #else
-    // In Python 2, a file object's write() method does not return the number of
-    // bytes written, so let's just assume that everything has been written properly.
+    // In Python 2, a file object's write() method does not return the number
+    // of bytes written, so let's just assume that everything has been written
+    // properly.
     len = size * nmemb;
     #endif
+
     Py_DECREF(input);
     Py_DECREF(data);
+    _ZRAN_FILE_UTIL_RELEASE_GIL
     return (size_t) len / size;
 
 fail:
     Py_XDECREF(input);
     Py_XDECREF(data);
+    _ZRAN_FILE_UTIL_RELEASE_GIL
     return 0;
 }
 
 /*
- * Implements a method analogous to getc that is performed on Python file-like objects.
+ * Implements a method analogous to getc that is performed on Python file-like
+ * objects.
  */
 int _getc_python(PyObject *f) {
     char buf;
@@ -158,28 +226,33 @@ int _getc_python(PyObject *f) {
  * Calls ferror on fd if specified, otherwise the Python-specific method on f.
  */
 int ferror_(FILE *fd, PyObject *f) {
-    return fd != NULL ? ferror(fd): _ferror_python(f);
+    return fd != NULL ? ferror(fd) : _ferror_python(f);
 }
 
 /*
  * Calls fseek on fd if specified, otherwise the Python-specific method on f.
  */
 int fseek_(FILE *fd, PyObject *f, int64_t offset, int whence) {
-    return fd != NULL ? FSEEK(fd, offset, whence): _fseek_python(f, offset, whence);
+    return fd != NULL
+        ? FSEEK(fd, offset, whence)
+        : _fseek_python(f, offset, whence);
 }
 
 /*
  * Calls ftell on fd if specified, otherwise the Python-specific method on f.
  */
 int64_t ftell_(FILE *fd, PyObject *f) {
-    return fd != NULL ? FTELL(fd): _ftell_python(f);
+    return fd != NULL ? FTELL(fd) : _ftell_python(f);
 }
 
 /*
  * Calls fread on fd if specified, otherwise the Python-specific method on f.
  */
 size_t fread_(void *ptr, size_t size, size_t nmemb, FILE *fd, PyObject *f) {
-    return fd != NULL ? fread(ptr, size, nmemb, fd): _fread_python(ptr, size, nmemb, f);
+
+  return fd != NULL
+      ? fread(ptr, size, nmemb, fd)
+      : _fread_python(ptr, size, nmemb, f);
 }
 
 /*
@@ -201,8 +274,14 @@ int fflush_(FILE *fd, PyObject *f) {
 /*
  * Calls fwrite on fd if specified, otherwise the Python-specific method on f.
  */
-size_t fwrite_(const void *ptr, size_t size, size_t nmemb, FILE *fd, PyObject *f) {
-    return fd != NULL ? fwrite(ptr, size, nmemb, fd): _fwrite_python(ptr, size, nmemb, f);
+size_t fwrite_(const void *ptr,
+               size_t      size,
+               size_t      nmemb,
+               FILE       *fd,
+               PyObject   *f) {
+    return fd != NULL
+        ? fwrite(ptr, size, nmemb, fd)
+        : _fwrite_python(ptr, size, nmemb, f);
 }
 
 /*
diff --git a/indexed_gzip/zran_file_util.h b/indexed_gzip/zran_file_util.h
index 9c458b00..5bd8bd8a 100644
--- a/indexed_gzip/zran_file_util.h
+++ b/indexed_gzip/zran_file_util.h
@@ -4,8 +4,6 @@
 /*
  * File utilities used to manipulate either
  * Python file-like objects or file descriptors.
- *
- * Author: Paul McCarthy <pauldmccarthy@gmail.com>
  */
 
 #include <stdlib.h>
@@ -15,42 +13,50 @@
 #include <Python.h>
 
 /*
- * Implements a method analogous to fread that is performed on Python file-like objects.
+ * Implements a method analogous to fread that is performed on Python
+ * file-like objects.
  */
 size_t _fread_python(void *ptr, size_t size, size_t nmemb, PyObject *f);
 
 /*
- * Implements a method analogous to ftell that is performed on Python file-like objects.
+ * Implements a method analogous to ftell that is performed on Python
+ * file-like objects.
  */
 int64_t _ftell_python(PyObject *f);
 
 /*
- * Implements a method analogous to fseek that is performed on Python file-like objects.
+ * Implements a method analogous to fseek that is performed on Python
+ * file-like objects.
  */
 int _fseek_python(PyObject *f, int64_t offset, int whence);
 
 /*
- * Implements a method analogous to feof that is performed on Python file-like objects.
+ * Implements a method analogous to feof that is performed on Python file-like
+ * objects.
  */
 int _feof_python(PyObject *f, size_t f_ret);
 
 /*
- * Implements a method analogous to ferror that is performed on Python file-like objects.
+ * Implements a method analogous to ferror that is performed on Python
+ * file-like objects.
  */
 int _ferror_python(PyObject *f);
 
 /*
- * Implements a method analogous to fflush that is performed on Python file-like objects.
+ * Implements a method analogous to fflush that is performed on Python
+ * file-like objects.
  */
 int _fflush_python(PyObject *f);
 
 /*
- * Implements a method analogous to fwrite that is performed on Python file-like objects.
+ * Implements a method analogous to fwrite that is performed on Python
+ * file-like objects.
  */
 size_t _fwrite_python(const void *ptr, size_t size, size_t nmemb, PyObject *f);
 
 /*
- * Implements a method analogous to getc that is performed on Python file-like objects.
+ * Implements a method analogous to getc that is performed on Python
+ * file-like objects.
  */
 int _getc_python(PyObject *f);
 
@@ -87,7 +93,8 @@ int fflush_(FILE *fd, PyObject *f);
 /*
  * Calls fwrite on fd if specified, otherwise the Python-specific method on f.
  */
-size_t fwrite_(const void *ptr, size_t size, size_t nmemb, FILE *fd, PyObject *f);
+size_t fwrite_(
+    const void *ptr, size_t size, size_t nmemb, FILE *fd, PyObject *f);
 
 /*
  * Calls getc on fd if specified, otherwise the Python-specific method on f.
@@ -95,4 +102,4 @@ size_t fwrite_(const void *ptr, size_t size, size_t nmemb, FILE *fd, PyObject *f
 int getc_(FILE *fd, PyObject *f);
 
 
-#endif /* __ZRAN_H__ */
+#endif /* __ZRAN_FILE_UTIL_H__ */
diff --git a/indexed_gzip/zran_file_util.pxd b/indexed_gzip/zran_file_util.pxd
index 51df9d0b..8c04337d 100644
--- a/indexed_gzip/zran_file_util.pxd
+++ b/indexed_gzip/zran_file_util.pxd
@@ -1,8 +1,6 @@
 #
 # Cython declaration for the zran_file_util library.
 #
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
 
 from libc.stdio  cimport FILE
 from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t, int64_t


Test patch:
diff --git a/indexed_gzip/tests/__init__.py b/indexed_gzip/tests/__init__.py
index 2ef22527..bdb2276c 100644
--- a/indexed_gzip/tests/__init__.py
+++ b/indexed_gzip/tests/__init__.py
@@ -49,7 +49,7 @@ def poll(until):
     while not until():
         time.sleep(0.5)
         cur = time.time()
-        elapsed = cur - start
+        elapsed = int(round(cur - start))
         if int(elapsed) % 60 == 0:
             print('Waiting ({:0.2f} minutes)'.format(elapsed / 60.0))
 
diff --git a/indexed_gzip/tests/ctest_indexed_gzip.pyx b/indexed_gzip/tests/ctest_indexed_gzip.pyx
index e10344a0..a934e2ca 100644
--- a/indexed_gzip/tests/ctest_indexed_gzip.pyx
+++ b/indexed_gzip/tests/ctest_indexed_gzip.pyx
@@ -33,6 +33,7 @@ import indexed_gzip as igzip
 from . import gen_test_data
 from . import check_data_valid
 from . import tempdir
+from . import compress
 
 from libc.stdio cimport (SEEK_SET,
                          SEEK_CUR,
@@ -754,7 +755,7 @@ def test_import_export_index():
             with open(idxfname, 'wb') as idxf:
                 f.export_index(fileobj=idxf)
 
-        # Check that it works
+        # Check that we can read it back
         with igzip._IndexedGzipFile(fname) as f:
 
             # Should raise if wrong permissions
@@ -858,8 +859,9 @@ def test_picklable():
 
     with tempdir():
         data = np.random.randint(1, 1000, (10000, 10000), dtype=np.uint32)
-        with gzip.open(fname, 'wb') as f:
+        with open(fname+'.bin', 'wb') as f:
             f.write(data.tobytes())
+        compress(fname+'.bin', fname)
         del f
 
         gzf        = igzip.IndexedGzipFile(fname)
@@ -900,8 +902,9 @@ def test_copyable():
 
     with tempdir():
         data = np.random.randint(1, 1000, (10000, 10000), dtype=np.uint32)
-        with gzip.open(fname, 'wb') as f:
+        with open(fname+'.bin', 'wb') as f:
             f.write(data.tobytes())
+        compress(fname+'.bin', fname)
         del f
 
         gzf        = igzip.IndexedGzipFile(fname)