From 37682bc07beab24f0f4395bc5f33129bf3daf03d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl>
Date: Fri, 1 Mar 2024 10:58:16 +0100
Subject: [PATCH] * added AutoflushFile * added correct read() to
 DevNullFilelikeObject * Proxy got _get_object() * added tell(), seekable(),
 truncate() and seek() to DevNullFilelikeObject

---
 .gitignore                                    |   1 +
 CHANGELOG.md                                  |   6 +
 docs/files.rst                                |   6 +
 satella/__init__.py                           |   2 +-
 satella/coding/concurrent/atomic.py           |   8 +-
 satella/coding/concurrent/locked_dataset.py   |   4 +-
 satella/coding/concurrent/monitor.py          |   4 +-
 satella/coding/concurrent/sync.py             |  10 +-
 satella/coding/concurrent/thread.py           |   4 +-
 satella/coding/concurrent/timer.py            |   8 +-
 satella/coding/decorators/arguments.py        |   6 +-
 satella/coding/decorators/flow_control.py     |   4 +-
 satella/coding/sequences/iterators.py         |   6 +-
 .../structures/dictionaries/expiring.py       |  10 +-
 satella/coding/structures/hashable_objects.py |   2 +-
 satella/coding/structures/heaps/time.py       |   2 +-
 satella/coding/structures/proxy.py            |   6 +-
 satella/coding/structures/ranking.py          |   2 +-
 satella/coding/structures/singleton.py        |   2 +-
 satella/debug/tainting/environment.py         |   2 +-
 satella/files.py                              | 107 +++++++++++++++++-
 satella/os/__init__.py                        |   8 +-
 satella/time/__init__.py                      |   8 +-
 satella/time/backoff.py                       |   4 +-
 satella/time/measure.py                       |   2 +-
 satella/time/misc.py                          |   6 +-
 tests/test_coding/test_structures.py          |   1 +
 tests/test_files.py                           |  21 +++-
 28 files changed, 190 insertions(+), 62 deletions(-)

diff --git a/.gitignore b/.gitignore
index 3720f965..89b9ddbf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 ## Eclipse
 #################
 test*.json
+test*.txt
 .eggs
 satella.sublime*
 hs_err_pid*.log
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 73c99623..d73ad728 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1 +1,7 @@
 # v2.23.4
+
+* added AutoflushFile
+* added correct read() to DevNullFilelikeObject
+* Proxy got _get_object()
+* added tell(), seekable(), truncate() and seek() to DevNullFilelikeObject
+
diff --git a/docs/files.rst b/docs/files.rst
index 6e23482d..4a395c48 100644
--- a/docs/files.rst
+++ b/docs/files.rst
@@ -43,3 +43,9 @@ write_out_file_if_different
 ---------------------------
 
 .. autofunction:: satella.files.write_out_file_if_different
+
+AutoflushFile
+-------------
+
+.. autoclass:: satella.files.AutoflushFile
+    :members:
diff --git a/satella/__init__.py b/satella/__init__.py
index 4f66480f..1da7697f 100644
--- a/satella/__init__.py
+++ b/satella/__init__.py
@@ -1 +1 @@
-__version__ = '2.23.4a1'
+__version__ = '2.23.4b1'
diff --git a/satella/coding/concurrent/atomic.py b/satella/coding/concurrent/atomic.py
index e4aec734..53df01fe 100644
--- a/satella/coding/concurrent/atomic.py
+++ b/satella/coding/concurrent/atomic.py
@@ -1,9 +1,9 @@
 import typing as tp
 
-from .monitor import Monitor
-from .thread import Condition
-from ...exceptions import WouldWaitMore
-from ...time.measure import measure
+from satella.coding.concurrent.monitor import Monitor
+from satella.coding.concurrent.thread import Condition
+from satella.exceptions import WouldWaitMore
+from satella.time.measure import measure
 
 Number = tp.Union[int, float]
 
diff --git a/satella/coding/concurrent/locked_dataset.py b/satella/coding/concurrent/locked_dataset.py
index 2a17b2bc..36b48074 100644
--- a/satella/coding/concurrent/locked_dataset.py
+++ b/satella/coding/concurrent/locked_dataset.py
@@ -2,8 +2,8 @@ import inspect
 import threading
 import typing as tp
 
-from ..decorators import wraps
-from ...exceptions import ResourceLocked, ResourceNotLocked, WouldWaitMore
+from satella.coding.decorators import wraps
+from satella.exceptions import ResourceLocked, ResourceNotLocked, WouldWaitMore
 
 
 class LockedDataset:
diff --git a/satella/coding/concurrent/monitor.py b/satella/coding/concurrent/monitor.py
index 2dc44cec..de2df68b 100644
--- a/satella/coding/concurrent/monitor.py
+++ b/satella/coding/concurrent/monitor.py
@@ -3,9 +3,9 @@ import copy
 import threading
 import typing as tp
 
-from ..decorators.decorators import wraps
+from satella.coding.decorators.decorators import wraps
 
-from ..typing import K, V, T
+from satella.coding.typing import K, V, T
 
 
 class Monitor:
diff --git a/satella/coding/concurrent/sync.py b/satella/coding/concurrent/sync.py
index 55828239..0025c177 100644
--- a/satella/coding/concurrent/sync.py
+++ b/satella/coding/concurrent/sync.py
@@ -2,11 +2,11 @@ import time
 import typing as tp
 from concurrent.futures import wait, ThreadPoolExecutor
 
-from .atomic import AtomicNumber
-from .futures import ExecutorWrapper
-from .thread import Condition
-from ...exceptions import WouldWaitMore
-from ...time.measure import measure
+from satella.coding.concurrent.atomic import AtomicNumber
+from satella.coding.concurrent.futures import ExecutorWrapper
+from satella.coding.concurrent.thread import Condition
+from satella.exceptions import WouldWaitMore
+from satella.time.measure import measure
 
 
 def sync_threadpool(tpe: tp.Union[ExecutorWrapper, ThreadPoolExecutor],
diff --git a/satella/coding/concurrent/thread.py b/satella/coding/concurrent/thread.py
index 5303cdd6..5a35adb4 100644
--- a/satella/coding/concurrent/thread.py
+++ b/satella/coding/concurrent/thread.py
@@ -9,8 +9,8 @@ from concurrent.futures import Future
 from threading import Condition as PythonCondition
 
 from satella.coding.decorators import wraps
-from ..typing import ExceptionList
-from ...exceptions import ResourceLocked, WouldWaitMore
+from satella.coding.typing import ExceptionList
+from satella.exceptions import ResourceLocked, WouldWaitMore
 
 
 def call_in_separate_thread(*t_args, no_thread_attribute: bool = False,
diff --git a/satella/coding/concurrent/timer.py b/satella/coding/concurrent/timer.py
index 87c51807..4847ece0 100644
--- a/satella/coding/concurrent/timer.py
+++ b/satella/coding/concurrent/timer.py
@@ -4,10 +4,10 @@ import threading
 import time
 
 from satella.coding.recast_exceptions import log_exceptions
-from .monitor import Monitor
-from ..structures.heaps.time import TimeBasedHeap
-from ..structures.singleton import Singleton
-from ...time.parse import parse_time_string
+from satella.coding.concurrent.monitor import Monitor
+from satella.coding.structures.heaps.time import TimeBasedHeap
+from satella.coding.structures.singleton import Singleton
+from satella.time.parse import parse_time_string
 
 logger = logging.getLogger(__name__)
 
diff --git a/satella/coding/decorators/arguments.py b/satella/coding/decorators/arguments.py
index cf257893..8d7bcf03 100644
--- a/satella/coding/decorators/arguments.py
+++ b/satella/coding/decorators/arguments.py
@@ -5,9 +5,9 @@ import typing as tp
 from inspect import Parameter
 
 from satella.coding.typing import T, Predicate
-from .decorators import wraps
-from ..misc import source_to_function, get_arguments, call_with_arguments, _get_arguments
-from ..predicates import PredicateClass, build_structure
+from satella.coding.decorators.decorators import wraps
+from satella.coding.misc import source_to_function, get_arguments, call_with_arguments, _get_arguments
+from satella.coding.predicates import PredicateClass, build_structure
 
 U = tp.TypeVar('U')
 
diff --git a/satella/coding/decorators/flow_control.py b/satella/coding/decorators/flow_control.py
index 039c9c3b..f25420c5 100644
--- a/satella/coding/decorators/flow_control.py
+++ b/satella/coding/decorators/flow_control.py
@@ -1,8 +1,8 @@
 import queue
 import typing as tp
 
-from .decorators import wraps
-from ..typing import ExceptionClassType, NoArgCallable, Predicate
+from satella.coding.decorators.decorators import wraps
+from satella.coding.typing import ExceptionClassType, NoArgCallable, Predicate
 
 Queue = tp.TypeVar('Queue')
 
diff --git a/satella/coding/sequences/iterators.py b/satella/coding/sequences/iterators.py
index 5fabe827..c589ac53 100644
--- a/satella/coding/sequences/iterators.py
+++ b/satella/coding/sequences/iterators.py
@@ -3,9 +3,9 @@ import itertools
 import typing as tp
 import warnings
 
-from ..decorators import for_argument, wraps
-from ..recast_exceptions import rethrow_as, silence_excs
-from ..typing import Iteratable, T, U, Predicate, V, K
+from satella.coding.decorators import for_argument, wraps
+from satella.coding.recast_exceptions import rethrow_as, silence_excs
+from satella.coding.typing import Iteratable, T, U, Predicate, V, K
 
 
 def iterate_callable(clbl: tp.Callable[[int], V], start_from: int = 0,
diff --git a/satella/coding/structures/dictionaries/expiring.py b/satella/coding/structures/dictionaries/expiring.py
index 718e4c8d..67c7c4db 100644
--- a/satella/coding/structures/dictionaries/expiring.py
+++ b/satella/coding/structures/dictionaries/expiring.py
@@ -4,11 +4,11 @@ import typing as tp
 import weakref
 from abc import ABCMeta, abstractmethod
 
-from ..heaps import TimeBasedSetHeap
-from ..singleton import Singleton
-from ...concurrent.monitor import Monitor
-from ...recast_exceptions import rethrow_as, silence_excs
-from ...typing import K, V, NoArgCallable
+from satella.coding.structures.heaps import TimeBasedSetHeap
+from satella.coding.structures.singleton import Singleton
+from satella.coding.concurrent.monitor import Monitor
+from satella.coding.recast_exceptions import rethrow_as, silence_excs
+from satella.coding.typing import K, V, NoArgCallable
 
 
 class Cleanupable(metaclass=ABCMeta):
diff --git a/satella/coding/structures/hashable_objects.py b/satella/coding/structures/hashable_objects.py
index 09b61812..635484b0 100644
--- a/satella/coding/structures/hashable_objects.py
+++ b/satella/coding/structures/hashable_objects.py
@@ -1,4 +1,4 @@
-from .proxy import Proxy
+from satella.coding.structures.proxy import Proxy
 
 
 class HashableWrapper(Proxy):
diff --git a/satella/coding/structures/heaps/time.py b/satella/coding/structures/heaps/time.py
index 5404b0d3..b7e77c9e 100644
--- a/satella/coding/structures/heaps/time.py
+++ b/satella/coding/structures/heaps/time.py
@@ -4,7 +4,7 @@ import typing as tp
 
 from satella.coding.recast_exceptions import rethrow_as
 from satella.coding.typing import T, Number, NoArgCallable
-from .base import Heap
+from satella.coding.structures.heaps.base import Heap
 
 
 class TimeBasedHeap(Heap):
diff --git a/satella/coding/structures/proxy.py b/satella/coding/structures/proxy.py
index d63f4c6c..54ec9b5a 100644
--- a/satella/coding/structures/proxy.py
+++ b/satella/coding/structures/proxy.py
@@ -61,6 +61,10 @@ class Proxy(tp.Generic[T]):
         self.__obj = object_to_wrap  # type: T
         self.__wrap_operations = wrap_operations  # type: bool
 
+    def _get_object(self) -> T:
+        """For your internal application usage, primarily extending Proxy classes"""
+        return self.__obj
+
     def __call__(self, *args, **kwargs):
         return self.__obj(*args, **kwargs)
 
@@ -79,7 +83,7 @@ class Proxy(tp.Generic[T]):
         else:
             setattr(self.__obj, key, value)
 
-    def __getattr__(self, item):
+    def __getattr__(self, item: str):
         return getattr(self.__obj, item)
 
     def __delattr__(self, item) -> None:
diff --git a/satella/coding/structures/ranking.py b/satella/coding/structures/ranking.py
index bcfd551f..6b2619fa 100644
--- a/satella/coding/structures/ranking.py
+++ b/satella/coding/structures/ranking.py
@@ -2,7 +2,7 @@ import collections
 import typing as tp
 
 from satella.coding.typing import T
-from .sorted_list import SortedList
+from satella.coding.structures.sorted_list import SortedList
 
 
 class Ranking(tp.Generic[T]):
diff --git a/satella/coding/structures/singleton.py b/satella/coding/structures/singleton.py
index 49a7bff4..3dc9e831 100644
--- a/satella/coding/structures/singleton.py
+++ b/satella/coding/structures/singleton.py
@@ -1,6 +1,6 @@
 import typing as tp
 
-from ..decorators.decorators import wraps
+from satella.coding.decorators.decorators import wraps
 
 
 # noinspection PyPep8Naming
diff --git a/satella/debug/tainting/environment.py b/satella/debug/tainting/environment.py
index 57d2cdde..f9352bd2 100644
--- a/satella/debug/tainting/environment.py
+++ b/satella/debug/tainting/environment.py
@@ -10,7 +10,7 @@ import warnings
 
 from satella.coding.typing import T
 
-from .tainteds import TaintedObject, access_tainted, taint
+from satella.debug.tainting.tainteds import TaintedObject, access_tainted, taint
 
 local = threading.local()
 
diff --git a/satella/files.py b/satella/files.py
index cd38ebc1..7a4430f1 100644
--- a/satella/files.py
+++ b/satella/files.py
@@ -1,38 +1,65 @@
+from __future__ import annotations
 import codecs
 import io
 import os
 import re
 import shutil
+import types
+import typing
 import typing as tp
 
 __all__ = ['read_re_sub_and_write', 'find_files', 'split', 'read_in_file', 'write_to_file',
            'write_out_file_if_different', 'make_noncolliding_name', 'try_unlink',
-           'DevNullFilelikeObject', 'read_lines']
+           'DevNullFilelikeObject', 'read_lines', 'AutoflushFile']
 
 from satella.coding.recast_exceptions import silence_excs
+from satella.coding.structures import Proxy
 from satella.coding.typing import Predicate
 
 SEPARATORS = {'\\', '/'}
 SEPARATORS.add(os.path.sep)
 
 
-class DevNullFilelikeObject:
+class DevNullFilelikeObject(io.FileIO):
     """
     A /dev/null filelike object. For multiple uses.
+
+    :param binary: is this a binary file
     """
-    __slots__ = 'is_closed',
+    __slots__ = 'is_closed', 'binary'
 
-    def __init__(self):
+    def __init__(self, binary: bool = False):
         self.is_closed = False
+        self.binary = binary
+
+    def tell(self) -> int:
+        """Return the current file offset"""
+        return 0
+
+    def truncate(self, __size: tp.Optional[int] = None) -> int:
+        """Truncate file to __size starting bytes"""
+        return 0
+
+    def writable(self) -> bool:
+        """Is this object writable"""
+        return True
+
+    def seek(self, v: int) -> int:
+        """Seek to a particular file offset"""
+        return 0
+
+    def seekable(self) -> bool:
+        """Is this file seekable?"""
+        return True
 
-    def read(self, byte_count: tp.Optional[int] = None):
+    def read(self, byte_count: tp.Optional[int] = None) -> tp.Union[str, bytes]:
         """
         :raises ValueError: this object has been closed
         :raises io.UnsupportedOperation: since reading from this is forbidden
         """
         if self.is_closed:
             raise ValueError('Reading from closed /dev/null!')
-        raise io.UnsupportedOperation('read')
+        return b'' if self.binary else ''
 
     def write(self, x: tp.Union[str, bytes]) -> int:
         """
@@ -320,3 +347,71 @@ def write_out_file_if_different(path: str, data: tp.Union[bytes, str],
     except FileNotFoundError:
         write_to_file(path, data, encoding)
         return True
+
+
+class AutoflushFile(Proxy[io.FileIO]):
+    """
+    A file that is supposed to be closed after each write command issued. Use like:
+
+    >>> f = AutoflushFile('test.txt', 'rb+', encoding='utf-8')
+    >>> f.write('test')
+    >>> with open('test.txt', 'a+', encoding='utf-8') as fin:
+    >>>     assert fin.read() == 'test'
+    """
+
+    def __init__(self, file, mode='r', *con_args, **con_kwargs):
+        object.__setattr__(self, 'con_kwargs', con_kwargs)
+        object.__setattr__(self, 'pointer', None)
+
+        if mode in ('w+', 'wb+'):
+            fle = open(*(file, 'wb'))
+            fle.truncate(0)
+            fle.close()
+
+        mode = {'w': 'a', 'wb': 'ab', 'w+': 'a+', 'wb+': 'ab+', 'a': 'a', 'ab': 'ab'}[mode]
+
+        object.__setattr__(self, 'con_args', (file, mode, *con_args))
+        fle = self._open_file()
+        super().__init__(fle)
+        object.__setattr__(self, 'pointer', fle.tell())
+
+    def read(self, *args, **kwargs) -> tp.Union[str, bytes]:
+        """Read a file, returning the read-in data"""
+        file = self._open_file()
+        p = file.read(*args, **kwargs)
+        self.__dict__['pointer'] = file.tell()
+        self._close_file()
+        return p
+
+    def _get_file(self) -> tp.Optional[AutoflushFile]:
+        return self.__dict__.get('_Proxy__obj')
+
+    def _open_file(self) -> open:
+        file = self._get_file()
+        if file is None:
+            file = open(*self.con_args, **self.con_kwargs)
+            ptr = file.tell()
+            self.__dict__['_Proxy__obj'] = file
+            self.__dict__['pointer'] = ptr
+        return file
+
+    def _close_file(self) -> None:
+        file = self._get_file()
+        if file is not None:
+            ptr = file.tell()
+            self.__dict__['pointer'] = ptr
+            file.close()
+            self.__dict__['_Proxy__obj'] = None
+
+    def close(self) -> None:
+        """Closes the file"""
+        self._open_file()
+        self._close_file()
+
+    def write(self, *args, **kwargs) -> int:
+        """Write a particular value to the file, close it afterwards."""
+        file = self._open_file()
+        val = file.write(*args, **kwargs)
+        self.__dict__['pointer'] = file.tell()
+        self._close_file()
+        return val
diff --git a/satella/os/__init__.py b/satella/os/__init__.py
index 92f6c89c..e06e8f70 100644
--- a/satella/os/__init__.py
+++ b/satella/os/__init__.py
@@ -1,7 +1,7 @@
-from .daemon import daemonize
-from .misc import suicide, is_running_as_root, whereis
-from .pidlock import PIDFileLock
-from .signals import hang_until_sig
+from satella.os.daemon import daemonize
+from satella.os.misc import suicide, is_running_as_root, whereis
+from satella.os.pidlock import PIDFileLock
+from satella.os.signals import hang_until_sig
 
 __all__ = [
     'daemonize', 'whereis',
diff --git a/satella/time/__init__.py b/satella/time/__init__.py
index 58cd3680..927f9b32 100644
--- a/satella/time/__init__.py
+++ b/satella/time/__init__.py
@@ -1,7 +1,7 @@
-from .measure import measure, TimeSignal
-from .misc import time_us, time_ms, time_as_int, sleep
-from .parse import parse_time_string
-from .backoff import ExponentialBackoff
+from satella.time.measure import measure, TimeSignal
+from satella.time.misc import time_us, time_ms, time_as_int, sleep
+from satella.time.parse import parse_time_string
+from satella.time.backoff import ExponentialBackoff
 
 __all__ = ['measure', 'TimeSignal', 'ExponentialBackoff', 'time_us', 'time_ms',
            'time_as_int', 'parse_time_string', 'sleep']
diff --git a/satella/time/backoff.py b/satella/time/backoff.py
index 1bcabd23..d152d10c 100644
--- a/satella/time/backoff.py
+++ b/satella/time/backoff.py
@@ -6,8 +6,8 @@ from satella.coding.decorators.decorators import wraps
 from satella.coding.typing import ExceptionList
 
 from satella.coding.concurrent.thread import Condition
-from .measure import measure
-from ..exceptions import WouldWaitMore
+from satella.time.measure import measure
+from satella.exceptions import WouldWaitMore
 
 
 class ExponentialBackoff:
diff --git a/satella/time/measure.py b/satella/time/measure.py
index 08220a43..06131b8a 100644
--- a/satella/time/measure.py
+++ b/satella/time/measure.py
@@ -1,5 +1,5 @@
 from __future__ import annotations
-from ..exceptions import WouldWaitMore
+from satella.exceptions import WouldWaitMore
 import typing as tp
 import time
 import copy
diff --git a/satella/time/misc.py b/satella/time/misc.py
index a94723ee..b194c17e 100644
--- a/satella/time/misc.py
+++ b/satella/time/misc.py
@@ -1,12 +1,12 @@
 import time
 import typing as tp
 
-__all__ = ['time_as_int', 'time_ms', 'sleep', 'time_us', 'ExponentialBackoff']
+__all__ = ['time_as_int', 'time_ms', 'sleep', 'time_us']
 
 from satella.coding.concurrent.thread import Condition
 from satella.exceptions import WouldWaitMore
-from .parse import parse_time_string
-from .measure import measure
+from satella.time.parse import parse_time_string
+from satella.time.measure import measure
 
 
 def sleep(y: tp.Union[str, float], abort_on_interrupt: bool = False) -> bool:
diff --git a/tests/test_coding/test_structures.py b/tests/test_coding/test_structures.py
index a5748928..f796e369 100644
--- a/tests/test_coding/test_structures.py
+++ b/tests/test_coding/test_structures.py
@@ -372,6 +372,7 @@ class TestStructures(unittest.TestCase):
 
     def test_proxy(self):
         a = Proxy(2)
+        self.assertIs(a._get_object(), 2)
         self.assertEqual(int(a), 2)
         self.assertEqual(a, 2)
         self.assertEqual(a+2, 4)
diff --git a/tests/test_files.py b/tests/test_files.py
index 02c4b09c..d0adccda 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -6,7 +6,7 @@ import unittest
 import shutil
 from satella.files import read_re_sub_and_write, find_files, split, read_in_file, write_to_file, \
     write_out_file_if_different, make_noncolliding_name, try_unlink, DevNullFilelikeObject, \
-    read_lines
+    read_lines, AutoflushFile
 
 
 def putfile(path: str) -> None:
@@ -19,6 +19,17 @@ class TestFiles(unittest.TestCase):
     def test_read_nonexistent_file(self):
         self.assertRaises(FileNotFoundError, lambda: read_in_file('moot'))
 
+    def test_autoflush_file(self):
+        af = AutoflushFile('test3.txt', 'w+', encoding='utf-8')
+        try:
+            af.write('test')
+            assert read_in_file('test3.txt', encoding='utf-8') == 'test'
+            af.write('test2')
+            assert read_in_file('test3.txt', encoding='utf-8') == 'testtest2'
+        finally:
+            af.close()
+            try_unlink('test3.txt')
+
     def test_read_lines(self):
         lines = read_lines('LICENSE')
         self.assertTrue(all(lines))
@@ -35,9 +46,13 @@ class TestFiles(unittest.TestCase):
     def test_devnullfilelikeobject(self):
         null = DevNullFilelikeObject()
         self.assertEqual(null.write('ala'), 3)
+        assert null.seek(0) == 0
+        assert null.tell() == 0
+        assert null.seekable()
+        assert null.truncate(0) == 0
         self.assertEqual(null.write(b'ala'), 3)
-        self.assertRaises(io.UnsupportedOperation, lambda: null.read())
-        self.assertRaises(io.UnsupportedOperation, lambda: null.read(7))
+        self.assertEqual(null.read(), '')
+        self.assertEqual(null.read(7), '')
         null.flush()
         null.close()
         self.assertRaises(ValueError, lambda: null.write('test'))
-- 
GitLab