Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: byte streams were destroyed by wrapper #18

Merged
merged 6 commits into from
Oct 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
10 changes: 9 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mutapath/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from mutapath.defaults import PathDefaults
from mutapath.immutapath import Path
from mutapath.mutapath import MutaPath
6 changes: 3 additions & 3 deletions mutapath/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)

Expand Down
17 changes: 17 additions & 0 deletions mutapath/defaults.py
Original file line number Diff line number Diff line change
@@ -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
57 changes: 42 additions & 15 deletions mutapath/immutapath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -26,22 +27,26 @@
except NotImplementedError:
SerializableType = object

POSIX_ENABLED_DEFAULT = False
STRING_REPR = False


@path_wrapper
class Path(SerializableType):
"""Immutable Path"""
_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__()

Expand All @@ -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)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -223,15 +233,25 @@ 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., '/').

:Example:
>>> 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` """
Expand Down Expand Up @@ -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:
"""
Expand Down
6 changes: 3 additions & 3 deletions mutapath/mutapath.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
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
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)
Expand Down
101 changes: 76 additions & 25 deletions tests/helper.py
Original file line number Diff line number Diff line change
@@ -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.")
Expand All @@ -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)
Loading