diff --git a/foo.txt b/foo.txt new file mode 100644 index 00000000..1856e9be --- /dev/null +++ b/foo.txt @@ -0,0 +1 @@ +Hello, World \ No newline at end of file diff --git a/fs/patch.py b/fs/patch.py new file mode 100644 index 00000000..1aa8c2d1 --- /dev/null +++ b/fs/patch.py @@ -0,0 +1 @@ +from ._patch import install \ No newline at end of file diff --git a/fs/patch/__init__.py b/fs/patch/__init__.py new file mode 100644 index 00000000..ba5890f9 --- /dev/null +++ b/fs/patch/__init__.py @@ -0,0 +1,2 @@ +from ._install import install +from ._patch import patch \ No newline at end of file diff --git a/fs/patch/__main__.py b/fs/patch/__main__.py new file mode 100644 index 00000000..e3bc7efd --- /dev/null +++ b/fs/patch/__main__.py @@ -0,0 +1,24 @@ +from __future__ import unicode_literals + +import logging + +logging.basicConfig(level=logging.DEBUG) + +from . import patch + + +from fs import open_fs + +fs = open_fs('mem://') +fs.touch('foo') +fs.makedir('bar').settext('egg', 'Hello, World!') + +import os + +with patch(fs): + print(os.listdir('/')) + print(os.getcwd()) + os.chdir('bar') + print(os.listdir('.')) + print(open('egg').read()) + diff --git a/fs/patch/_install.py b/fs/patch/_install.py new file mode 100644 index 00000000..370231b9 --- /dev/null +++ b/fs/patch/_install.py @@ -0,0 +1,14 @@ +from .patch_builtins import PatchBuiltins +from .patch_os import PatchOS + + +installed = False + +def install(): + """Install patcher.""" + global installed + if not installed: + PatchBuiltins().install() + PatchOS().install() + installed = True + diff --git a/fs/patch/_patch.py b/fs/patch/_patch.py new file mode 100644 index 00000000..d3e5bc8f --- /dev/null +++ b/fs/patch/_patch.py @@ -0,0 +1,14 @@ +from __future__ import unicode_literals + +from .. import open_fs +from .base import PatchContext +from ._install import install + + +def patch(fs_url, auto_close=True): + install() + if isinstance(fs_url, str): + fs_obj = open_fs(fs_url) + return PatchContext(fs_obj, auto_close=True) + else: + return PatchContext(fs_url, auto_close=auto_close) diff --git a/fs/patch/base.py b/fs/patch/base.py new file mode 100644 index 00000000..cd9d9c60 --- /dev/null +++ b/fs/patch/base.py @@ -0,0 +1,135 @@ +from __future__ import unicode_literals + +import logging +import os +from types import TracebackType +from typing import List, Optional, Type + +from ..base import FS +from ..path import join + + +log = logging.getLogger("fs.patch") + + +class NotPatched(Exception): + pass + + +def original(method): + _patch = method._fspatch + original = _patch['original'] + return original + + +class Patch(object): + + stack = [] # type: List[PatchContext] + + def get_module(self): + raise NotImplementedError() + + @classmethod + def method(cls): + def deco(f): + f._fspatch = {} + return f + return deco + + @classmethod + def push(self, context): + self.stack.append(context) + + @classmethod + def pop(self): + return self.stack.pop() + + @property + def is_patched(self): + return bool(self.stack) + + @property + def context(self): + if self.stack: + return self.stack[-1] + else: + raise NotPatched() + + @property + def fs(self): + if self.stack: + return self.stack[-1].fs_obj + else: + raise NotPatched() + + @property + def cwd(self): + if self.stack: + return self.stack[-1].cwd + else: + raise NotPatched() + + @property + def os_cwd(self): + if not self.stack: + raise NotPatched() + return self.to_syspath(self.stack[-1].cwd) + + def from_cwd(self, path): + _path = self.to_fspath(path) + abs_path = join(self.cwd, _path) + return abs_path + + def to_fspath(self, syspath): + return syspath.replace(os.sep, '/') + + def to_syspath(self, path): + return path.replace('/', os.sep) + + def _chdir(self, path): + context = self.context + new_cwd = os.path.join(context.cwd, path) + context.cwd = new_cwd + + def make_syspath(path): + + os.path.join(context.cwd, path) + + def install(self): + module = self.get_module() + + for method_name, unbound_method in self.__class__.__dict__.items(): + _patch = getattr(unbound_method, "_fspatch", None) + if _patch is None: + continue + method = getattr(self, method_name) + _patch["original"] = method + + setattr(module, method_name, method) + + +class PatchContext(object): + + def __init__(self, fs_obj, auto_close=False): + self.fs_obj = fs_obj + self.auto_close = auto_close + self.cwd = "/" + + def __repr__(self): + return "".format(self.fs_obj, self.auto_close, self.cwd) + + def __enter__(self): + Patch.push(self) + log.debug('%r patched', self) + return self + + def __exit__( + self, + exc_type, # type: Optional[Type[BaseException]] + exc_value, # type: Optional[BaseException] + traceback, # type: Optional[TracebackType] + ): + context = Patch.pop() + log.debug('%r un-patched', context) + if self.auto_close: + context.fs_obj.close() diff --git a/fs/patch/patch_builtins.py b/fs/patch/patch_builtins.py new file mode 100644 index 00000000..082a3ecb --- /dev/null +++ b/fs/patch/patch_builtins.py @@ -0,0 +1,42 @@ +from __future__ import unicode_literals + +from .base import NotPatched, Patch +from .translate_errors import raise_os + +class PatchBuiltins(Patch): + def get_module(self): + import builtins + return builtins + + @Patch.method() + def open( + self, + file, + mode="r", + buffering=-1, + encoding=None, + errors=None, + newline=None, + closefd=True, + opener=None, + ): + try: + return self.fs.open( + self.from_cwd(file), + mode=mode, + buffering=buffering, + encoding=encoding, + errors=errors, + newline=newline, + ) + except NotPatched: + return original(open)( + file, + mode=mode, + buffering=buffering, + encoding=encoding, + errors=errors, + newline=newline, + closefd=closefd, + opener=opener, + ) diff --git a/fs/patch/patch_os.py b/fs/patch/patch_os.py new file mode 100644 index 00000000..4a55c735 --- /dev/null +++ b/fs/patch/patch_os.py @@ -0,0 +1,37 @@ +from __future__ import unicode_literals + +import os + +from six import PY2 + +from .base import original, Patch +from .translate_errors import raise_os +from .. import errors + + +class PatchOS(Patch): + def get_module(self): + import os + return os + + @Patch.method() + def chdir(self, path): + if not self.is_patched: + return original(chdir(path)) + with raise_os(): + return self._chdir(path) + + @Patch.method() + def getcwd(self): + if not self.is_patched: + return original(getcwd)() + return self.os_cwd + + @Patch.method() + def listdir(self, path): + if not self.is_patched: + return original(listdir)(path) + _path = self.from_cwd(path) + with raise_os(): + dirlist = self.fs.listdir(_path) + return dirlist \ No newline at end of file diff --git a/fs/patch/translate_errors.py b/fs/patch/translate_errors.py new file mode 100644 index 00000000..1b80a082 --- /dev/null +++ b/fs/patch/translate_errors.py @@ -0,0 +1,14 @@ +from contextlib import contextmanager + +from .. import errors + + +@contextmanager +def raise_os(): + try: + yield + except errors.ResourceNotFound as error: + if PY2: + raise IOError(2, "No such file or directory") + else: + raise FileNotFoundError(2, "No such file or directory: {!r}".format(error.path))