147 lines
4.1 KiB
Python
147 lines
4.1 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""FileReadBackwards module."""
|
|
|
|
import io
|
|
import os
|
|
|
|
from .buffer_work_space import BufferWorkSpace
|
|
|
|
supported_encodings = ["utf-8", "ascii", "latin-1"] # any encodings that are backward compatible with ascii should work
|
|
|
|
|
|
class FileReadBackwards:
|
|
|
|
"""Class definition for `FileReadBackwards`.
|
|
|
|
A `FileReadBackwards` will spawn a `FileReadBackwardsIterator` and keep an opened file handler.
|
|
|
|
It can be used as a Context Manager. If done so, when exited, it will close its file handler.
|
|
|
|
In any mode, `close()` can be called to close the file handler..
|
|
|
|
Args:
|
|
|
|
Returns:
|
|
|
|
"""
|
|
|
|
def __init__(self, path, encoding="utf-8", chunk_size=io.DEFAULT_BUFFER_SIZE):
|
|
"""Constructor for FileReadBackwards.
|
|
|
|
Args:
|
|
path: Path to the file to be read
|
|
encoding (str): Encoding
|
|
chunk_size (int): How many bytes to read at a time
|
|
"""
|
|
if encoding.lower() not in supported_encodings:
|
|
error_message = "{0} encoding was not supported/tested.".format(encoding)
|
|
error_message += "Supported encodings are '{0}'".format(",".join(supported_encodings))
|
|
raise NotImplementedError(error_message)
|
|
|
|
self.path = path
|
|
self.encoding = encoding.lower()
|
|
self.chunk_size = chunk_size
|
|
self.iterator = FileReadBackwardsIterator(io.open(self.path, mode="rb"), self.encoding, self.chunk_size)
|
|
|
|
def __iter__(self):
|
|
"""Return its iterator."""
|
|
return self.iterator
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""Closes all opened its file handler and propagates all exceptions on exit."""
|
|
self.close()
|
|
return False
|
|
|
|
def close(self):
|
|
"""Closes all opened it s file handler."""
|
|
self.iterator.close()
|
|
|
|
def readline(self):
|
|
""" """
|
|
|
|
try:
|
|
r = next(self.iterator) + os.linesep
|
|
return r
|
|
except StopIteration:
|
|
return ""
|
|
|
|
|
|
class FileReadBackwardsIterator:
|
|
"""Iterator for `FileReadBackwards`.
|
|
|
|
This will read backwards line by line a file. It holds an opened file handler.
|
|
|
|
Args:
|
|
|
|
Returns:
|
|
|
|
"""
|
|
def __init__(self, fp, encoding, chunk_size):
|
|
"""Constructor for FileReadBackwardsIterator
|
|
|
|
Args:
|
|
fp (File): A file that we wish to start reading backwards from
|
|
encoding (str): Encoding of the file
|
|
chunk_size (int): How many bytes to read at a time
|
|
"""
|
|
self.path = fp.name
|
|
self.encoding = encoding
|
|
self.chunk_size = chunk_size
|
|
self.__fp = fp
|
|
self.__buf = BufferWorkSpace(self.__fp, self.chunk_size)
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def next(self):
|
|
"""Returns unicode string from the last line until the beginning of file.
|
|
|
|
Gets exhausted if::
|
|
|
|
* already reached the beginning of the file on previous iteration
|
|
* the file got closed
|
|
|
|
When it gets exhausted, it closes the file handler.
|
|
|
|
Args:
|
|
|
|
Returns:
|
|
|
|
"""
|
|
# Using binary mode, because some encodings such as "utf-8" use variable number of
|
|
# bytes to encode different Unicode points.
|
|
# Without using binary mode, we would probably need to understand each encoding more
|
|
# and do the seek operations to find the proper boundary before issuing read
|
|
if self.closed:
|
|
raise StopIteration
|
|
if self.__buf.has_returned_every_line():
|
|
self.close()
|
|
raise StopIteration
|
|
self.__buf.read_until_yieldable()
|
|
r = self.__buf.return_line()
|
|
return r.decode(self.encoding)
|
|
|
|
__next__ = next
|
|
|
|
@property
|
|
def closed(self):
|
|
"""The status of the file handler.
|
|
|
|
:return: True if the file handler is still opened. False otherwise.
|
|
|
|
Args:
|
|
|
|
Returns:
|
|
|
|
"""
|
|
return self.__fp.closed
|
|
|
|
def close(self):
|
|
"""Closes the file handler."""
|
|
self.__fp.close()
|