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