diff --git a/Lib/mailbox.py b/Lib/mailbox.py --- a/Lib/mailbox.py +++ b/Lib/mailbox.py @@ -1864,40 +1864,125 @@ """Message with MMDF-specific properties.""" -class _ProxyFile: - """A read-only wrapper of a file.""" +class _ProxyFile(io.RawIOBase): + """A io.RawIOBase inheriting read-only wrapper for a seekable file. + It supports __iter__() and the context-manager protocol. + """ + def __init__(self, file, pos=None): + """If pos is not None then the file will keep track of its position.""" + self._file = file + self._pos = pos + self._trackpos = True if pos is not None else False + self._close = True + self._is_open = True - def __init__(self, f, pos=None): - """Initialize a _ProxyFile.""" - self._file = f - if pos is None: - self._pos = f.tell() + def _set_noclose(self): + """Subclass hook - use to avoid closing internal file object.""" + self._close = False + + def _closed_check(self): + """Raise ValueError if not open.""" + if not self._is_open: + raise ValueError('I/O operation on closed file') + + def close(self): + if self._close: + self._close = False + self._file.close() + del self._file + self._is_open = False + + @property + def closed(self): + return not self._is_open + + def _write_check(self): + """Raises io.UnsupportedOperation.""" + raise io.UnsupportedOperation('ProxyFile is readonly') + + #def fileno(self): + def flush(self): + self._closed_check() + self._write_check() + + #def isatty(self): + + def _read(self, size, read_method, add_arg=None): + if size < 0: + size = -1 + if self._trackpos: + self._file.seek(self._pos) + if not add_arg: + result = read_method(size) else: - self._pos = pos + result = read_method(size, add_arg) + if self._trackpos: + self._pos = self._file.tell() + return result - def read(self, size=None): - """Read bytes.""" - return self._read(size, self._file.read) + def readable(self): + self._closed_check() + return self._file.readable() - def read1(self, size=None): - """Read bytes.""" - return self._read(size, self._file.read1) - - def readline(self, size=None): - """Read a line.""" + def readline(self, size=-1): + self._closed_check() return self._read(size, self._file.readline) - def readlines(self, sizehint=None): - """Read multiple lines.""" + def readlines(self, sizehint=-1): result = [] for line in self: result.append(line) - if sizehint is not None: + if sizehint >= 0: sizehint -= len(line) if sizehint <= 0: break return result + def read(self, size=-1): + self._closed_check() + return self._read(size, self._file.read) + + #def readall(self): + + def readinto(self, by_arr): + self._closed_check() + return self._read(len(by_arr), self._file.readinto, by_arr) + + def seekable(self): + self._closed_check() + return True + + def seek(self, offset, whence=0): + self._closed_check() + if whence == 1: + if not self._trackpos: + self._pos = self._file.tell() + self._file.seek(self._pos) + self._pos = self._file.seek(offset, whence) + return self._pos + + def tell(self): + self._closed_check() + if not self._trackpos: + self._pos = self._file.tell() + return self._pos + + def truncate(self, size=None): + self._closed_check() + self._write_check() + + def writable(self): + self._closed_check() + return False + + def writelines(self, lines): + self._closed_check() + self._write_check() + + def write(self, by_arr): + self._closed_check() + self._write_check() + def __iter__(self): """Iterate over lines.""" while True: @@ -1906,32 +1991,6 @@ raise StopIteration yield line - def tell(self): - """Return the position.""" - return self._pos - - def seek(self, offset, whence=0): - """Change position.""" - if whence == 1: - self._file.seek(self._pos) - self._file.seek(offset, whence) - self._pos = self._file.tell() - - def close(self): - """Close the file.""" - if hasattr(self._file, 'close'): - self._file.close() - del self._file - - def _read(self, size, read_method): - """Read size bytes using read_method.""" - if size is None: - size = -1 - self._file.seek(self._pos) - result = read_method(size) - self._pos = self._file.tell() - return result - def __enter__(self): """Context manager protocol support.""" return self @@ -1939,29 +1998,16 @@ def __exit__(self, *exc): self.close() - def readable(self): - return self._file.readable() - - def writable(self): - return self._file.writable() - - def seekable(self): - return self._file.seekable() - - def flush(self): - return self._file.flush() - - @property - def closed(self): - return self._file.closed - class _PartialFile(_ProxyFile): """A read-only wrapper of part of a file.""" def __init__(self, f, start=None, stop=None): """Initialize a _PartialFile.""" + if start is None: + start = f.tell() _ProxyFile.__init__(self, f, start) + super()._set_noclose() self._start = start self._stop = stop @@ -1971,6 +2017,7 @@ def seek(self, offset, whence=0): """Change position, possibly with respect to start or stop.""" + self._closed_check() if whence == 0: self._pos = self._start whence = 1 @@ -1988,11 +2035,6 @@ size = remaining return _ProxyFile._read(self, size, read_method) - def close(self): - # do *not* close the underlying file object for partial files, - # since it's global to the mailbox object - del self._file - def _lock_file(f, dotlock=True): """Lock file f using lockf and dot locking.""" diff --git a/Lib/test/test_mailbox.py b/Lib/test/test_mailbox.py --- a/Lib/test/test_mailbox.py +++ b/Lib/test/test_mailbox.py @@ -290,12 +290,14 @@ key1 = self._box.add(_sample_message) with self._box.get_file(key0) as file: data0 = file.read() - with self._box.get_file(key1) as file: - data1 = file.read() + file1 = self._box.get_file(key1) + data1 = file1.read() self.assertEqual(data0.decode('ascii').replace(os.linesep, '\n'), self._template % 0) self.assertEqual(data1.decode('ascii').replace(os.linesep, '\n'), _sample_message) + file1.close() + file1.close() def test_iterkeys(self): # Get keys using iterkeys() @@ -1833,10 +1835,35 @@ self.assertFalse(proxy.read()) def _test_close(self, proxy): - # Close a file + self.assertFalse(proxy.closed) + self.assertTrue(proxy.seekable()) + self.assertRaises(io.UnsupportedOperation, proxy.flush) + self.assertRaises(io.UnsupportedOperation, proxy.truncate, 0) + self.assertFalse(proxy.writable()) + self.assertRaises(io.UnsupportedOperation, proxy.writelines, ['AU']) + self.assertRaises(io.UnsupportedOperation, proxy.write, 'AU') + proxy.close() - self.assertRaises(AttributeError, lambda: proxy.close()) + self.assertTrue(proxy.closed) + try: + proxy.close() + except: + self.fail('Proxy.close() failure') + self.assertRaises(ValueError, proxy.flush) + self.assertRaises(ValueError, proxy.readable) + self.assertRaises(ValueError, proxy.readline) + self.assertRaises(ValueError, proxy.readlines) + self.assertRaises(ValueError, proxy.read) + self.assertRaises(ValueError, proxy.readall) + self.assertRaises(ValueError, proxy.readinto, bytearray()) + self.assertRaises(ValueError, proxy.seekable) + self.assertRaises(ValueError, proxy.seek, 0) + self.assertRaises(ValueError, proxy.tell) + self.assertRaises(ValueError, proxy.truncate) + self.assertRaises(ValueError, proxy.writable) + self.assertRaises(ValueError, proxy.writelines, ['AU']) + self.assertRaises(ValueError, proxy.write, 'AU') class TestProxyFile(TestProxyFileBase):