diff --git a/.gitignore b/.gitignore index 3720f965beadec28418ea49bbe0b4b42ceb2a8c5..89b9ddbf014ba784e52bc0919e4d851cd00d8c4f 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 73c99623f388ed9f59da5ba20518ba48ee72eb25..d73ad728543310101b0793af31a902f14e0fb058 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 6e23482dc851f76788b50f6335e9f9379ed86bca..4a395c48bdfdbe46c15d341cebe8032011d63dde 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 4f66480f86868a9eb0902bbfbad6efb33ea6649e..1da7697f9c5986c12b09c70eaa185a2c638c09d7 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 e4aec73409dce6bcfd0acb77116e8f7c4bea1068..53df01fe11cc87539dba1df41fafb19b0ea181be 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 2a17b2bc80e3b62b9596a36c5861128ee30a51c9..36b4807441fa378f0ac517439dd697b17c1444e2 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 2dc44cec3e8599c740bc23a1969f33f87f285bb1..de2df68b48540befbc2423f9350368aea486d47c 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 558282391a1096cc376d61dfc67d9ac4e4454cbd..0025c177365821033cf68c60000b2d617fb02bd5 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 5303cdd6bf8e6e224e27c15e54c6f90cf0a1e84f..5a35adb474cbc2cff574645b864b3f5870e86530 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 87c518076cbdd9a4370f1bc5937e394605daf77e..4847ece079c3b8c68c63cbda77873d95214b987d 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 cf25789308bb6124430532f20e723d4a22725c20..8d7bcf03684d70504e84065f4a314dbe1c58a8d3 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 039c9c3bf2d9d5e673e7097d4e7f34e7d4c184a3..f25420c5b006c38b4f73616f68ba2962e2769d5c 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 5fabe827551b30411dc7f80b0cf359612346a9ab..c589ac53abc90ddfc9c128dd6f3a1798f9ac2fcd 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 718e4c8de25f1eb6ac1eee2c81476bfe6dc1cc3b..67c7c4db0b8d0fc5f3692124c8a26b545c0ec0e9 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 09b618121c12c653fd9113d3342c52810fbf0719..635484b0714aba9585029d1ff418236372c62a6f 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 5404b0d3bfd32dd986cd70658d2021581dfb01dc..b7e77c9e86f02f12c5015f5fc3dd385ec544fffb 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 d63f4c6cbb97f7c84f6086927e1039717e9003d1..54ec9b5aa9fc3feb785a97f531f89288560dd1dd 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 bcfd551fa222d21f33bcb9cecfecd64a5f636d74..6b2619fa3d89e4382e224ed60d946c840d6918be 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 49a7bff45283af6451bae3f4e33a68fb9f6bb3d1..3dc9e83169f44234ac86e7e1ee41708856a7fa92 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 57d2cdde81d58352b31df3e71055fe69426951cd..f9352bd2d0a3d42399a34f750e09d559cb8e1f9c 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 cd38ebc1765ae4edd3d9f98f1e822db64754571f..7a4430f1fb6ea9d75a5900e402a800e9b0554ca1 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 92f6c89c86cc522a0b01a5dd8e32abfa10689d60..e06e8f70503d727b846fa44b9326cb5b94d11f16 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 58cd36804b0166955b1a0cbd1ae11337b75fe73e..927f9b32421eb6be29fa27649df640025eb91bc8 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 1bcabd233ba8f72a50997a0eb145b98cf7eedeee..d152d10ca79c4659881617f982f0cf9ca4725a4b 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 08220a4363df240c8a230e2e6c5387475806c1b1..06131b8ab07e6cb4aa32cf54e404233521485ad5 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 a94723ee651bca3901eb62d69ae3c1ab75a157f8..b194c17e7d8e03e5e7dd200b498069f894649085 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 a5748928e5260e10dae75fd5d884b552d3f35eb3..f796e3692bfbe3166dd3d33f130a4c308558b27c 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 02c4b09c3edbed21de65589c6829e95417058bd5..d0adccdafdfb9d0a9c4dd8b99c8a5122b86947c3 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'))