diff --git a/.gitignore b/.gitignore index 49bad4a9adcf19af864a6aa517c80ff8317c99d8..b7522274b68c279154e6049d7fef8a887b10c1f2 100644 --- a/.gitignore +++ b/.gitignore @@ -165,14 +165,13 @@ var sdist develop-eggs .installed.cfg - +test_files/ # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox - #Translations *.mo diff --git a/CHANGELOG.md b/CHANGELOG.md index e8dc83ada9ea563d0d413fb26a84e920eba7fd56..f7902b9f3a8e78391e3287e505962ea4253d3594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,5 @@ * add JSONAbleDataObject * improved typing for reraise_as +* added weak_refs to SingletonWithRegardsTo +* added jump_to_directory diff --git a/docs/files.rst b/docs/files.rst index 7f2b1b01b59dc72cd52a9a21f03a9e1f7c26f7ab..718dc9220820f313a32584fec1f57eed2c4fc97a 100644 --- a/docs/files.rst +++ b/docs/files.rst @@ -9,6 +9,14 @@ A file-like object that will dispose of your content. .. autoclass:: satella.files.DevNullFilelikeObject :members: + +jump_to_directory +----------------- + +.. autofunction:: jump_to_directory + :members: + + safe_listdir ------------ diff --git a/satella/__init__.py b/satella/__init__.py index 66d2dcd3f0a45fb4b85fc5ce1cffddbfadcb3b11..b0f93f54dca729afce51939170ad8d7a63e2c0b3 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.25.4a3' +__version__ = '2.25.4' diff --git a/satella/coding/structures/singleton.py b/satella/coding/structures/singleton.py index 3dc9e83169f44234ac86e7e1ee41708856a7fa92..0a7609d8cfbbf1cbd65bc7f89bdb8ed9f4bfaf92 100644 --- a/satella/coding/structures/singleton.py +++ b/satella/coding/structures/singleton.py @@ -1,4 +1,5 @@ import typing as tp +import weakref from satella.coding.decorators.decorators import wraps @@ -36,7 +37,7 @@ def Singleton(cls): # noinspection PyPep8Naming -def SingletonWithRegardsTo(num_args: int): +def SingletonWithRegardsTo(num_args: int, weak_refs: bool = False): """ Make a memoized singletion depending on the arguments. @@ -55,6 +56,12 @@ def SingletonWithRegardsTo(num_args: int): >>> c = MyClass('dev1') >>> assert a is c >>> assert b is not c + + :param num_args: number of arguments to consume + :param weak_refs: if True, then singleton will be stored within a weak dictionary, so that it cleans up after itself + when the values are gone. + + .. warning:: If you set weak_refs to False and have a potentially unbounded number of arguments, you better watch out. """ def inner(cls): @@ -65,7 +72,10 @@ def SingletonWithRegardsTo(num_args: int): def singleton_new(cls, *args, **kw): it = cls.__dict__.get('__it__') if it is None: - it = cls.__it__ = {} + if weak_refs: + it = cls.__it__ = weakref.WeakValueDictionary() + else: + it = cls.__it__ = {} key = args[:num_args] if key in it: diff --git a/satella/files.py b/satella/files.py index 2fb00052dfeab4fc9881ab6fd202a396e2cb66cb..04a5b488da4a1c1342a04c6e04910265d515e2fd 100644 --- a/satella/files.py +++ b/satella/files.py @@ -10,7 +10,8 @@ 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', 'AutoflushFile'] + 'DevNullFilelikeObject', 'read_lines', 'AutoflushFile', + 'jump_to_directory'] from satella.coding import wraps from satella.coding.recast_exceptions import silence_excs, reraise_as @@ -37,6 +38,36 @@ def value_error_on_closed_file(getter): closed_devnull = value_error_on_closed_file(lambda y: y.is_closed) +class jump_to_directory(object): + """ + This will temporarily change current working directory. Note however is doesn't proof you against deliberately + changing the working directory by the user. + + Non existing directories will be created. + + :ivar path: (str) target path + :ivar prev_path: (str) path that was here before this was called. + """ + + __slots__ = 'path', 'prev_path' + + def __init__(self, path: tp.Optional[str], mode=0o777): + self.path = path + self.prev_path = None + os.makedirs(self.path, mode=mode, exist_ok=True) + + def __enter__(self): + self.prev_path = os.getcwd() + os.chdir(self.path) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + assert self.prev_path is not None + with reraise_as(FileNotFoundError): + os.chdir(self.prev_path) + return False + + class DevNullFilelikeObject(io.FileIO): """ A /dev/null filelike object. For multiple uses. diff --git a/tests/test_coding/test_singleton.py b/tests/test_coding/test_singleton.py index d4079f1e8d7cebb36d5acc832aef29769343f305..91ea5188dc60653b36d3957f5fbe6819d37078dc 100644 --- a/tests/test_coding/test_singleton.py +++ b/tests/test_coding/test_singleton.py @@ -1,3 +1,4 @@ +import gc import queue import unittest @@ -57,3 +58,24 @@ class TestSingleton(unittest.TestCase): self.assertEqual(set(get_instances_for_singleton(MyClass)), {('a',), ('b',)}) delete_singleton_for(MyClass, 'a') self.assertEqual(set(get_instances_for_singleton(MyClass)), {('b',)}) + + def test_singleton_with_regards_to_weak_refs(self): + instantiations = 0 + @SingletonWithRegardsTo(num_args=1, weak_refs=True) + class MyClass: + def __init__(self, device_id: str): + nonlocal instantiations + self.device_id = device_id + instantiations += 1 + + a = MyClass('a') + b = MyClass('b') + c = MyClass('a') + self.assertEqual(instantiations, 2) + del a + a = MyClass('a') + self.assertEqual(instantiations, 2) + del b + gc.collect() + b = MyClass('b') + self.assertEqual(instantiations, 3) diff --git a/tests/test_files.py b/tests/test_files.py index 7bc2a6416729bbe39ca47fd19f9824213f6cf7da..e0de6d7b3af889e13600995e9fa735c8cabb0a1c 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, AutoflushFile + read_lines, AutoflushFile, jump_to_directory def putfile(path: str) -> None: @@ -16,6 +16,12 @@ def putfile(path: str) -> None: class TestFiles(unittest.TestCase): + def test_monotonous(self): + with jump_to_directory('test/path'): + path = os.getcwd() + self.assertTrue(path.endswith('path')) + self.assertTrue(os.path.exists('path')) + def test_read_nonexistent_file(self): self.assertRaises(FileNotFoundError, lambda: read_in_file('moot'))