diff --git a/Pipfile b/Pipfile index 94d3047..772c55c 100644 --- a/Pipfile +++ b/Pipfile @@ -17,3 +17,4 @@ mashumaro = "==1.7" path-py = "==12.0.1" filelock = "==3.0.12" cached-property = "==1.5.1" +singletons = "==0.2.3" diff --git a/Pipfile.lock b/Pipfile.lock index fa46c3d..26d8119 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "35e0de744675ec036e94f8d69f99a53fed6a2a2568328a47971a13fad24f2cd0" + "sha256": "353ea0c644a3daed63dbe6f01cde2e380ea223b5d3ac9df7339e526ca01b8dfe" }, "pipfile-spec": 6, "requires": {}, @@ -51,6 +51,14 @@ "index": "pypi", "version": "==12.0.1" }, + "singletons": { + "hashes": [ + "sha256:94b7e8a3edbae48e05f0ac960c6bc0aa7820702584732149ac73e5f81f23abfb", + "sha256:d1fa48be24bb301ae6f4a4be10f4a4535f4618b154aac26bb3b1d1e0996eb6cf" + ], + "index": "pypi", + "version": "==0.2.3" + }, "zipp": { "hashes": [ "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", diff --git a/mutapath/__init__.py b/mutapath/__init__.py index 3d12d01..e7c9fa2 100644 --- a/mutapath/__init__.py +++ b/mutapath/__init__.py @@ -1,2 +1,3 @@ +from mutapath.defaults import PathDefaults from mutapath.immutapath import Path from mutapath.mutapath import MutaPath diff --git a/mutapath/decorator.py b/mutapath/decorator.py index 51f7dc5..64cb969 100644 --- a/mutapath/decorator.py +++ b/mutapath/decorator.py @@ -19,7 +19,7 @@ "__delattr__", "__setattr__", "__getattr__", "joinpath", "clone", "__exit__", "__fspath__", "'_Path__wrap_attribute'", "__wrap_decorator", "_op_context", "__hash__", "__enter__", "_norm", "open", "lock", "getcwd", "dirname", "owner", "uncshare", "posix_format", "posix_string", "__add__", "__radd__", "_set_contained", - "with_poxis_enabled", "_hash_cache", "_serialize", "_deserialize" + "with_poxis_enabled", "_hash_cache", "_serialize", "_deserialize", "string_repr_enabled", "_shorten_duplicates" ] __MUTABLE_FUNCTIONS = {"rename", "renames", "copy", "copy2", "copyfile", "copymode", "copystat", "copytree", "move", @@ -74,9 +74,9 @@ def __wrap_decorator(self, *args, **kwargs): return None converter = __path_converter(self.clone) - if isinstance(result, List) and not isinstance(result, str): + if isinstance(result, List) and not isinstance(result, (str, bytes, bytearray)): return list(map(converter, result)) - if isinstance(result, Iterable) and not isinstance(result, str): + if isinstance(result, Iterable) and not isinstance(result, (str, bytes, bytearray)): return (converter(g) for g in result) return __path_converter(self.clone)(result) diff --git a/mutapath/defaults.py b/mutapath/defaults.py new file mode 100644 index 0000000..42ddbfd --- /dev/null +++ b/mutapath/defaults.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + +import singletons + + +@dataclass +class PathDefaults(metaclass=singletons.ThreadSingleton): + """ + This dataclass contains all defaults that are used for paths if no arguments are given. + """ + + posix: bool = False + string_repr: bool = False + + def reset(self): + self.posix = False + self.string_repr = False diff --git a/mutapath/immutapath.py b/mutapath/immutapath.py index 742d13d..88814cd 100644 --- a/mutapath/immutapath.py +++ b/mutapath/immutapath.py @@ -7,7 +7,7 @@ import shutil import warnings from contextlib import contextmanager -from typing import Union, Iterable, ClassVar, Callable, Optional +from typing import Union, Iterable, Callable, Optional import filelock import path @@ -16,6 +16,7 @@ import mutapath from mutapath.decorator import path_wrapper +from mutapath.defaults import PathDefaults from mutapath.exceptions import PathException from mutapath.lock_dummy import DummyFileLock @@ -26,9 +27,6 @@ except NotImplementedError: SerializableType = object -POSIX_ENABLED_DEFAULT = False -STRING_REPR = False - @path_wrapper class Path(SerializableType): @@ -36,12 +34,19 @@ class Path(SerializableType): _contained: Union[path.Path, pathlib.PurePath, str] = path.Path("") __always_posix_format: bool __string_repr: bool - __mutable: ClassVar[object] + __mutable: object def __init__(self, contained: Union[Path, path.Path, pathlib.PurePath, str] = "", *, - posix: bool = POSIX_ENABLED_DEFAULT, string_repr: bool = STRING_REPR): + posix: Optional[bool] = None, + string_repr: Optional[bool] = None): + if posix is None: + posix = PathDefaults().posix self.__always_posix_format = posix + + if string_repr is None: + string_repr = PathDefaults().string_repr self.__string_repr = string_repr + self._set_contained(contained, posix) super().__init__() @@ -53,11 +58,10 @@ def _set_contained(self, contained: Union[Path, path.Path, pathlib.PurePath, str contained = str(contained) normalized = path.Path.module.normpath(contained) - if posix is None: - if self.__always_posix_format: - normalized = Path.posix_string(normalized) - elif posix: + if (posix is None and self.__always_posix_format) or posix: normalized = Path.posix_string(normalized) + else: + normalized = Path._shorten_duplicates(normalized) contained = path.Path(normalized) @@ -92,12 +96,12 @@ def __setattr__(self, key, value): def __repr__(self): if self.__string_repr: return self.__str__() - return repr(self._contained) + return Path._shorten_duplicates(repr(self._contained)) def __str__(self): if self.posix_enabled: return self.posix_string() - return self._contained + return self._shorten_duplicates() def __eq__(self, other): if isinstance(other, pathlib.PurePath): @@ -208,7 +212,13 @@ def clone(self, contained) -> Path: :param contained: the new contained path element :return: the cloned path """ - return Path(contained, posix=self.__always_posix_format) + return Path(contained, posix=self.__always_posix_format, string_repr=self.__string_repr) + + @path.multimethod + def _shorten_duplicates(self, input_path: str = "") -> str: + if isinstance(input_path, Path): + input_path = input_path._contained + return input_path.replace('\\\\', '\\') @path.multimethod def posix_string(self, input_path: str = "") -> str: @@ -223,7 +233,7 @@ def posix_string(self, input_path: str = "") -> str: input_path = input_path._contained return input_path.replace('\\\\', '\\').replace('\\', '/') - def with_poxis_enabled(self, enable: bool = True): + def with_poxis_enabled(self, enable: bool = True) -> Path: """ Clone this path in posix format with posix-like separators (i.e., '/'). @@ -231,7 +241,17 @@ def with_poxis_enabled(self, enable: bool = True): >>> Path("\\home\\\\doe/folder\\sub").with_poxis_enabled() Path('/home/joe/doe/folder/sub') """ - return Path(self, posix=enable) + return Path(self, posix=enable, string_repr=self.__string_repr) + + def with_string_repr_enabled(self, enable: bool = True) -> Path: + """ + Clone this path in with string representation enabled. + + :Example: + >>> Path("/home/doe/folder/sub").with_string_repr_enabled() + '/home/joe/doe/folder/sub' + """ + return Path(self, posix=self.__always_posix_format, string_repr=enable) def with_name(self, new_name) -> Path: """ .. seealso:: :func:`pathlib.PurePath.with_name` """ @@ -312,6 +332,13 @@ def home(self) -> Path: return parent.name return self.drive + @property + def string_repr_enabled(self) -> bool: + """ + If set to True, the the representation of this path will always be returned unwrapped as the path's string. + """ + return self.__string_repr + @property def posix_enabled(self) -> bool: """ diff --git a/mutapath/mutapath.py b/mutapath/mutapath.py index 99f668d..26f67a1 100644 --- a/mutapath/mutapath.py +++ b/mutapath/mutapath.py @@ -1,13 +1,12 @@ from __future__ import annotations import pathlib -from typing import Optional, Union +from typing import Union, Optional import path import mutapath from mutapath.decorator import mutable_path_wrapper -from mutapath.immutapath import POSIX_ENABLED_DEFAULT, STRING_REPR @mutable_path_wrapper @@ -15,7 +14,8 @@ class MutaPath(mutapath.Path): """Mutable Path""" def __init__(self, contained: Union[MutaPath, mutapath.Path, path.Path, pathlib.PurePath, str] = "", *, - posix: Optional[bool] = POSIX_ENABLED_DEFAULT, string_repr: bool = STRING_REPR): + posix: Optional[bool] = None, + string_repr: Optional[bool] = None): if isinstance(contained, MutaPath): contained = contained._contained super(MutaPath, self).__init__(contained, posix=posix, string_repr=string_repr) diff --git a/tests/helper.py b/tests/helper.py index ee6977e..9ce70ee 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,17 +1,87 @@ import unittest from functools import wraps +from typing import Callable, Optional import path from mutapath import Path -def file_test(equal: bool = True, instance: bool = True, exists: bool = True, posix_test: bool = True): +class PathTest(unittest.TestCase): + test_path = "test_path" + + def __init__(self, *args): + super().__init__(*args) + + def _gen_start_path(self, posix: bool = False, string_repr: bool = False): + self.test_base = Path.getcwd() / self.test_path + self.test_base.rmtree_p() + self.test_base.mkdir() + new_file = self.test_base / "test.file" + new_file.touch() + with_string_repr = Path(new_file, string_repr=string_repr) + return with_string_repr.with_poxis_enabled(posix) + + def _clean(self): + self.test_base.rmtree_p() + + def typed_instance_test(self, *instance): + for i in instance: + self.assertIsInstance(i, Path) + self.assertIsInstance(i._contained, path.Path) + + def arg_with_matrix(self, with_func: Callable[[Path, bool], Path], + test_func: Optional[Callable[[Path], bool]] = None, + with_func_default: bool = True, **kwargs): + """ + Use a matrix of keyed init arguments with their immutable with methods. + The first passed keyed argument should relate to the with method. + """ + if len(kwargs) < 1: + raise ValueError( + "The matrix requires at least the init value and its default that correlates with the with method.") + + first_key = next(iter(kwargs)) + first_default = kwargs[first_key] + remaining_kwargs = kwargs.copy() + del remaining_kwargs[first_key] + enabled_kwarg = dict() + enabled_kwarg[first_key] = True + disabled_kwarg = dict() + disabled_kwarg[first_key] = False + + default_path = Path("/A/B/other.txt", **remaining_kwargs) + enabled = Path("/A/B/other.txt", **enabled_kwarg, **remaining_kwargs) + disabled = Path("/A/B/other.txt", **disabled_kwarg, **remaining_kwargs) + + for path in default_path, enabled, disabled: + + default_case = with_func(path) + with_enabled = with_func(path, True) + with_disabled = with_func(path, False) + if with_func_default: + self.assertEqual(enabled, default_case) + else: + self.assertEqual(disabled, default_case) + self.assertEqual(enabled, with_enabled) + self.assertEqual(disabled, with_disabled) + self.typed_instance_test(default_case, with_enabled, with_disabled) + if test_func is not None: + self.assertEqual(first_default, test_func(default_path)) + self.assertTrue(test_func(enabled)) + self.assertFalse(test_func(disabled)) + self.assertTrue(test_func(with_enabled)) + self.assertFalse(test_func(with_disabled)) + self.assertTrue(test_func(with_enabled.clone("/"))) + self.assertFalse(test_func(with_disabled.clone("/"))) + + +def file_test(equal=True, instance=True, exists=True, posix_test=True, string_test=True): def file_test_decorator(func): @wraps(func) def func_wrapper(cls: PathTest): - def test_case(use_posix: bool = False) -> Path: - actual = cls._gen_start_path(use_posix) + def test_case(use_posix: bool = False, use_string: bool = False) -> Path: + actual = cls._gen_start_path(use_posix, use_string) expected = func(cls, actual) if equal: cls.assertIsNotNone(expected, "This test does not return the expected value. Fix the test.") @@ -27,31 +97,12 @@ def test_case(use_posix: bool = False) -> Path: if posix_test: test_path = test_case(use_posix=True) cls.assertTrue(test_path.posix_enabled, "the test file is not in posix format") + if string_test: + test_path = test_case(use_string=True) + cls.assertTrue(test_path.string_repr_enabled, "the test file is not using string representation") finally: cls._clean() return func_wrapper return file_test_decorator - - -class PathTest(unittest.TestCase): - test_path = "test_path" - - def __init__(self, *args): - super().__init__(*args) - - def _gen_start_path(self, posix: bool = False): - self.test_base = Path.getcwd() / self.test_path - self.test_base.rmtree_p() - self.test_base.mkdir() - new_file = self.test_base / "test.file" - new_file.touch() - return new_file.with_poxis_enabled(posix) - - def _clean(self): - self.test_base.rmtree_p() - - def typed_instance_test(self, instance): - self.assertIsInstance(instance, Path) - self.assertIsInstance(instance._contained, path.Path) diff --git a/tests/test_immutapath.py b/tests/test_immutapath.py index f9c20d3..96a8ded 100644 --- a/tests/test_immutapath.py +++ b/tests/test_immutapath.py @@ -3,7 +3,7 @@ import path -from mutapath import Path, MutaPath +from mutapath import Path, MutaPath, PathDefaults from tests.helper import PathTest @@ -64,18 +64,36 @@ def test_with_parent(self): self.assertEqual(expected, actual) self.typed_instance_test(actual) + @staticmethod + def _string_repr_enabled(path: Path): + return path.string_repr_enabled + + def test_with_string_repr_enabled(self): + self.arg_with_matrix(Path.with_string_repr_enabled, self._string_repr_enabled, string_repr=False) + self.arg_with_matrix(Path.with_string_repr_enabled, self._string_repr_enabled, string_repr=False, posix=False) + self.arg_with_matrix(Path.with_string_repr_enabled, self._string_repr_enabled, string_repr=False, posix=True) + + def test_defaults_with_string_repr(self): + getter = lambda p: p.string_repr_enabled + PathDefaults().string_repr = True + self.arg_with_matrix(Path.with_string_repr_enabled, getter, string_repr=True) + self.arg_with_matrix(Path.with_string_repr_enabled, getter, string_repr=True, posix=False) + self.arg_with_matrix(Path.with_string_repr_enabled, getter, string_repr=True, posix=True) + PathDefaults().reset() + def test_with_posix_enabled(self): - other = Path("/A/B/other.txt") - expected = Path("/A/B/other.txt", posix=True) - actual = other.with_poxis_enabled() - actual2 = other.with_poxis_enabled(True) - actual3 = other.with_poxis_enabled(False) - self.assertEqual(expected, actual) - self.assertEqual(expected, actual2) - self.assertEqual(other, actual3) - self.typed_instance_test(actual) - self.typed_instance_test(actual2) - self.typed_instance_test(actual3) + getter = lambda p: p.posix_enabled + self.arg_with_matrix(Path.with_poxis_enabled, getter, posix=False) + self.arg_with_matrix(Path.with_poxis_enabled, getter, posix=False, string_repr=False) + self.arg_with_matrix(Path.with_poxis_enabled, getter, posix=False, string_repr=True) + + def test_defaults_with_posix(self): + getter = lambda p: p.posix_enabled + PathDefaults().posix = True + self.arg_with_matrix(Path.with_poxis_enabled, getter, posix=True) + self.arg_with_matrix(Path.with_poxis_enabled, getter, posix=True, string_repr=False) + self.arg_with_matrix(Path.with_poxis_enabled, getter, posix=True, string_repr=True) + PathDefaults().reset() def test_static_joinpath(self): expected = Path("/A/B/C/D/other.txt") diff --git a/tests/test_mutapath.py b/tests/test_mutapath.py index 47ef957..5775984 100644 --- a/tests/test_mutapath.py +++ b/tests/test_mutapath.py @@ -1,7 +1,7 @@ from mutapath import MutaPath, Path from tests.helper import PathTest, file_test -file_test_no_asserts = file_test(equal=False, instance=False, exists=False, posix_test=False) +file_test_no_asserts = file_test(equal=False, instance=False, exists=False, posix_test=False, string_test=False) class TestMutaPath(PathTest): @@ -9,8 +9,8 @@ def __init__(self, *args): self.test_path = "mutapath_test" super().__init__(*args) - def _gen_start_path(self, posix: bool = False): - return MutaPath(super(TestMutaPath, self)._gen_start_path(posix), posix=posix) + def _gen_start_path(self, posix: bool = False, use_string: bool = False): + return MutaPath(super(TestMutaPath, self)._gen_start_path(posix), posix=posix, string_repr=use_string) @file_test_no_asserts def test_suffix(self, test_file: Path): @@ -163,8 +163,9 @@ def test_capsulation(self): self.assertEqual(expected, actual) def test_repr(self): - excpected = MutaPath("/A/B") - self.assertTrue(repr(excpected).startswith("Path")) + expected = "Path('/A/B')" + actual = MutaPath("/A/B", posix=True) + self.assertEqual(expected, repr(actual)) def test_hash(self): expected = hash(Path("/A/B"))