Skip to content

Commit

Permalink
feat: Integrate Pathlib Attributes (#11)
Browse files Browse the repository at this point in the history
* The chained attribute accessors are now using `path.Path` and now additionally `pathlib.Path` as fallback. This grants wider compatibility with both libraries.

* `Path.glob` uses the mechanism from `pathlib.Path.glob` now, instead of `path.Path.glob` because it is confusing that `path.Path.glob` does not accept the same recursive `**` wildcards.

* Chained properties are now implicitly fetched.

* All chained iterator returns are now converted to generators if they are no lists or strings.

* fix(issue): improving maintainability, closing #12
  • Loading branch information
matfax authored Oct 20, 2019
1 parent fd388cc commit 5f18c45
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 88 deletions.
78 changes: 56 additions & 22 deletions mutapath/decorator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"""
The decorators convert all returning types to mutapath.Path, or mutapath.MutaPath instances, respectively.
The following types are covered:
* Internal routines returning non-iterable types in mutapath
* All types returned from routines and properties from pathlib
* All types returned from routines and properties from path
"""
import functools
import inspect
from types import GeneratorType
from typing import List, Iterable, Callable
import pathlib
from typing import List, Iterable, Callable, Optional

import path

Expand All @@ -19,6 +26,12 @@
"basename", "abspath", "join", "joinpath", "normpath", "relpath", "realpath", "relpathto"}


def __is_mbm(member):
if isinstance(member, property):
return True
return __is_def(member)


def __is_def(member):
while isinstance(member, functools.partial):
member = member.func
Expand All @@ -29,7 +42,7 @@ def __is_def(member):

def __path_converter(const: Callable):
def convert_path(result):
if isinstance(result, path.Path):
if isinstance(result, (path.Path, pathlib.PurePath)):
return const(result)
return result

Expand All @@ -45,34 +58,55 @@ def wrap_decorator(cls, *args, **kwargs):
return wrap_decorator


def wrap_attribute(orig_func):
@functools.wraps(orig_func)
def __wrap_decorator(cls, *args, **kwargs):
result = orig_func(cls._contained, *args, **kwargs)
def wrap_attribute(orig_attr, fetcher: Optional[Callable] = None):
@functools.wraps(orig_attr)
def __wrap_decorator(self, *args, **kwargs):
fetched = self._contained
if fetcher is not None:
fetched = fetcher(fetched)

if isinstance(orig_attr, property):
result = orig_attr.__get__(fetched)
else:
result = orig_attr(fetched, *args, **kwargs)

if result is None:
return None

converter = __path_converter(self.clone)
if isinstance(result, List) and not isinstance(result, str):
return list(map(__path_converter(cls.clone), result))
return list(map(converter, result))
if isinstance(result, Iterable) and not isinstance(result, str):
return iter(map(__path_converter(cls.clone), result))
if isinstance(result, GeneratorType):
return map(__path_converter(cls.clone), result)
return __path_converter(cls.clone)(result)
return (converter(g) for g in result)
return __path_converter(self.clone)(result)

if isinstance(orig_attr, property):
return property(fget=__wrap_decorator)

return __wrap_decorator


def path_wrapper(cls):
members = inspect.getmembers(cls, __is_def)
for name, method in members:
member_names = list()
for name, method in inspect.getmembers(cls, __is_def):
if name not in __EXCLUDE_FROM_WRAPPING:
setattr(cls, name, __path_func(method))
member_names, _ = zip(*members)
for name, _ in inspect.getmembers(path.Path, __is_def):
member_names.append(name)
for name, _ in inspect.getmembers(path.Path, __is_mbm):
if not name.startswith("_") \
and name not in __EXCLUDE_FROM_WRAPPING \
and name not in member_names:
method = getattr(path.Path, name)
assert not hasattr(cls, name)
setattr(cls, name, wrap_attribute(method))
if not hasattr(cls, name):
setattr(cls, name, wrap_attribute(method))
member_names.append(name)
for name, _ in inspect.getmembers(pathlib.Path, __is_mbm):
if not name.startswith("_") \
and name not in __EXCLUDE_FROM_WRAPPING \
and name not in member_names:
method = getattr(pathlib.Path, name)
if not hasattr(cls, name):
setattr(cls, name, wrap_attribute(method, pathlib.Path))
return cls


Expand All @@ -86,13 +120,13 @@ def mutation_decorator(self, *args, **kwargs):
if isinstance(result, path.Path):
self._contained = result
return self
elif isinstance(result, mutapath.Path):
if isinstance(result, mutapath.Path):
self._contained = result._contained
return self
return result
else:
result = orig_func(self, *args, **kwargs)
return cls(result)

result = orig_func(self, *args, **kwargs)
return cls(result)

return mutation_decorator

Expand Down
76 changes: 20 additions & 56 deletions mutapath/immutapath.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from filelock import SoftFileLock

import mutapath
from mutapath.decorator import path_wrapper, wrap_attribute
from mutapath.decorator import path_wrapper
from mutapath.exceptions import PathException
from mutapath.lock_dummy import DummyFileLock

Expand All @@ -24,19 +24,20 @@
@path_wrapper
class Path(object):
"""Immutable Path"""
_contained: Union[path.Path, pathlib.Path, str] = path.Path("")
_contained: Union[path.Path, pathlib.PurePath, str] = path.Path("")
__always_posix_format: bool
__mutable: ClassVar[object]

def __init__(self, contained: Union[Path, path.Path, pathlib.Path, str] = "", posix: bool = POSIX_ENABLED_DEFAULT):
def __init__(self, contained: Union[Path, path.Path, pathlib.PurePath, str] = "",
posix: bool = POSIX_ENABLED_DEFAULT):
self.__always_posix_format = posix
self._set_contained(contained, posix)

def _set_contained(self, contained: Union[Path, path.Path, pathlib.Path, str], posix: Optional[bool] = None):
def _set_contained(self, contained: Union[Path, path.Path, pathlib.PurePath, str], posix: Optional[bool] = None):
if contained:
if isinstance(contained, Path):
contained = contained._contained
elif isinstance(contained, pathlib.Path):
elif isinstance(contained, pathlib.PurePath):
contained = str(contained)

normalized = path.Path.module.normpath(contained)
Expand All @@ -53,10 +54,6 @@ def _set_contained(self, contained: Union[Path, path.Path, pathlib.Path, str], p
def __dir__(self) -> Iterable[str]:
return sorted(super(Path, self).__dir__()) + dir(path.Path)

def __getattr__(self, item):
attr = getattr(self._contained, item)
return wrap_attribute(attr)

def __setattr__(self, key, value):
if key == "_contained":
lock = self.__dict__.get("lock", None)
Expand All @@ -83,7 +80,7 @@ def __str__(self):
return self._contained

def __eq__(self, other):
if isinstance(other, pathlib.Path):
if isinstance(other, pathlib.PurePath):
other = str(other)
elif isinstance(other, Path):
other = other._contained
Expand Down Expand Up @@ -142,6 +139,14 @@ def __invert__(self):
from mutapath import MutaPath
return MutaPath(self._contained, self.posix_enabled)

@property
def to_pathlib(self) -> pathlib.Path:
"""
Return the contained path as pathlib.Path representation.
:return: the converted path
"""
return pathlib.Path(self._contained)

def clone(self, contained) -> Path:
"""
Clone this path with a new given wrapped path representation, having the same remaining attributes.
Expand Down Expand Up @@ -272,11 +277,6 @@ def suffix(self) -> str:
def suffix(self, value):
self._contained = self.with_suffix(value)

@property
def ext(self) -> str:
""" .. seealso:: :attr:`~mutapath.Path.suffix` """
return self._contained.ext

@property
def name(self) -> Path:
""" .. seealso:: :attr:`pathlib.PurePath.name` """
Expand All @@ -299,11 +299,6 @@ def base(self) -> Path:
def base(self, value):
self._contained = self.with_base(value)

@property
def uncshare(self) -> Path:
""" .. seealso:: :attr:`path.Path.uncshare` """
return Path(self._contained.uncshare)

@property
def stem(self) -> str:
""" .. seealso:: :attr:`pathlib.PurePath.stem` """
Expand All @@ -313,11 +308,6 @@ def stem(self) -> str:
def stem(self, value):
self._contained = self.with_stem(value)

@property
def drive(self) -> Path:
""" .. seealso:: :attr:`pathlib.PurePath.drive` """
return self.clone(self._contained.drive)

@property
def parent(self) -> Path:
""" .. seealso:: :attr:`pathlib.PurePath.parent` """
Expand All @@ -327,46 +317,20 @@ def parent(self) -> Path:
def parent(self, value):
self._contained = self.with_parent(value)

@property
def parents(self) -> Iterable[Path]:
""" .. seealso:: :attr:`pathlib.Path.parents` """
result = pathlib.Path(self._contained).parents
return iter(map(self.clone, result))

@property
def dirname(self) -> Path:
""" .. seealso:: :func:`os.path.dirname` """
return self.clone(self._contained.dirname())

@property
def size(self) -> int:
""" .. seealso:: :func:`os.path.getsize` """
return self._contained.size

@property
def ctime(self) -> float:
""" .. seealso:: :func:`os.path.getctime` """
return self._contained.ctime

@property
def mtime(self) -> float:
""" .. seealso:: :func:`os.path.getmtime` """
return self._contained.mtime

@property
def atime(self) -> float:
""" .. seealso:: :func:`os.path.getatime` """
return self._contained.atime

@property
def owner(self):
""" .. seealso:: :meth:`get_owner` """
return self._contained.owner

def open(self, *args, **kwargs):
""" .. seealso:: :func:`pathlib.Path.open` """
return io.open(str(self), *args, **kwargs)

def glob(self, pattern):
""" .. seealso:: :func:`pathlib.Path.glob` """
paths = self.to_pathlib.glob(pattern)
return (self.clone(g) for g in paths)

@cached_property
def lock(self) -> filelock.BaseFileLock:
"""
Expand Down
4 changes: 0 additions & 4 deletions mutapath/lock_dummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,12 @@ class DummyFileLock(BaseFileLock):

def release(self, force=False):
"""Doing nothing"""
pass

def acquire(self, timeout=None, poll_intervall=0.05):
"""Doing nothing"""
pass

def _acquire(self):
"""Doing nothing"""
pass

def _release(self):
"""Doing nothing"""
pass
2 changes: 1 addition & 1 deletion mutapath/mutapath.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class MutaPath(mutapath.Path):
"""Mutable Path"""

def __init__(self, contained: Union[MutaPath, mutapath.Path, path.Path, pathlib.Path, str] = "",
def __init__(self, contained: Union[MutaPath, mutapath.Path, path.Path, pathlib.PurePath, str] = "",
posix: Optional[bool] = POSIX_ENABLED_DEFAULT):
if isinstance(contained, MutaPath):
contained = contained._contained
Expand Down
25 changes: 25 additions & 0 deletions tests/test_immutapath.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,31 @@ def test_parents(self):
excpected = [Path("/A/B/C"), Path("/A/B"), Path("/A"), Path("/")]
actual = list(Path("/A/B/C/D").parents)
self.assertEqual(excpected, actual)
self.typed_instance_test(actual[0])

def test_anchor(self):
if os.name == 'nt':
excpected = "C:\\"
actual = Path("C:/A/B/C").anchor
else:
excpected = "/"
actual = Path("/A/B/C").anchor
self.assertEqual(excpected, actual)

def test_suffix(self):
excpected = ".bak"
actual = Path("file.txt.bak").suffix
self.assertEqual(excpected, actual)

def test_ext(self):
excpected = ".bak"
actual = Path("file.txt.bak").ext
self.assertEqual(excpected, actual)

def test_suffixes(self):
excpected = [".txt", ".bak"]
actual = Path("file.txt.bak").suffixes
self.assertEqual(excpected, actual)

def test_home(self):
excpected = Path("B")
Expand Down
35 changes: 30 additions & 5 deletions tests/test_with_path.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
import time
from types import GeneratorType
from typing import List

from mutapath import Path
from mutapath.exceptions import PathException
Expand All @@ -12,20 +14,43 @@ def __init__(self, *args):
super().__init__(*args)

@file_test(equal=False)
def test_wrapped_iterable(self, test_file: Path):
"""Verify that iterable nested functions have been mapped to the correct types"""
def test_wrapped_list(self, test_file: Path):
"""Verify that nested functions returning lists have been mapped to the correct types"""
expected = [test_file]
actual = self.test_base.listdir()
self.assertEqual(expected, actual)
self.typed_instance_test(actual[0])
self.assertIsInstance(actual, List)

@file_test(equal=False)
def test_wrapped_generator(self, test_file: Path):
"""Verify that nested generators have been mapped to the correct types"""
expected = [test_file]
actual = list(self.test_base.walk())
self.assertEqual(expected, actual)
self.typed_instance_test(actual[0])
actual = self.test_base.walk()
actual_list = list(actual)
self.assertEqual(expected, actual_list)
self.typed_instance_test(actual_list[0])
self.assertIsInstance(actual, GeneratorType)

@file_test(equal=False)
def test_glob(self, test_file: Path):
"""Verify that glob is returning the correct types"""
expected = [test_file]
actual = self.test_base.glob("*.file")
actual_list = list(actual)
self.assertEqual(expected, actual_list)
self.typed_instance_test(actual_list[0])
self.assertIsInstance(actual, GeneratorType)

@file_test(equal=False)
def test_rglob(self, test_file: Path):
"""Verify that rglob (which is fetched from pathlib.Path) is returning the correct types"""
expected = [test_file]
actual = self.test_base.rglob("*.file")
actual_list = list(actual)
self.assertEqual(expected, actual_list)
self.typed_instance_test(actual_list[0])
self.assertIsInstance(actual, GeneratorType)

@file_test(equal=False)
def test_open(self, test_file: Path):
Expand Down

0 comments on commit 5f18c45

Please sign in to comment.