From 9d15a0f4ca4fe397329e8eeaed43ca5498bff657 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 19 Aug 2018 17:54:36 +0100 Subject: [PATCH 1/5] new patch method --- foo.txt | 1 + fs/patch.py | 1 + fs/patch/__init__.py | 2 ++ fs/patch/__main__.py | 0 fs/patch/_install.py | 6 ++++ fs/patch/_patch.py | 12 +++++++ fs/patch/base.py | 74 ++++++++++++++++++++++++++++++++++++++ fs/patch/patch_builtins.py | 40 +++++++++++++++++++++ fs/patch/patch_os.py | 9 +++++ 9 files changed, 145 insertions(+) create mode 100644 foo.txt create mode 100644 fs/patch.py create mode 100644 fs/patch/__init__.py create mode 100644 fs/patch/__main__.py create mode 100644 fs/patch/_install.py create mode 100644 fs/patch/_patch.py create mode 100644 fs/patch/base.py create mode 100644 fs/patch/patch_builtins.py create mode 100644 fs/patch/patch_os.py 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..e69de29b diff --git a/fs/patch/_install.py b/fs/patch/_install.py new file mode 100644 index 00000000..2353774d --- /dev/null +++ b/fs/patch/_install.py @@ -0,0 +1,6 @@ + + +def install(): + """Install patcher.""" + GlobalsPatch().install() + diff --git a/fs/patch/_patch.py b/fs/patch/_patch.py new file mode 100644 index 00000000..6184b800 --- /dev/null +++ b/fs/patch/_patch.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals + +from .. import open_fs +from .base import PatchContext + + +def patch(fs_url, auto_close=True): + 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..5b9ca548 --- /dev/null +++ b/fs/patch/base.py @@ -0,0 +1,74 @@ +from __future__ import unicode_literals + +import logging +from types import TracebackType +from typing import List, Optional, Type + +from ..base import FS + + +logging.getLogger("fs.patch") + + +class NotPatched(Exception): + pass + + +def original(method): + _patch = method._fspatch + original = _patch['original'] + return original + + +def patch_method(): + def deco(f): + f._fspatch = {} + return f + return deco + + +class Patch(object): + + def __init__(self, module): + self.module = module + super(Patch, self).__init__() + + @property + def fs(self): + if PatchContext.stack: + return fs[-1].fs_obj + else: + raise NotPatch() + + def install(self): + for method_name in dir(self): + method = getattr(self, method_name) + _patch = getattr(method, "_fspatch", None) + if _patch is None: + continue + original = getattr(self.module, method_name) + _patch["original"] = original + setattr(self.module, method_name, method) + + +class PatchContext(object): + stack = [] # type: List[FS] + + def __init__(self, fs_obj, auto_close=False): + self.fs_obj = fs_obj + self.auto_close = auto_close + + def __enter__(self): + self.stack.append(self.fs_obj) + return self + + def __exit__( + self, + exc_type, # type: Optional[Type[BaseException]] + exc_value, # type: Optional[BaseException] + traceback, # type: Optional[TracebackType] + ): + fs_obj = self.stack.pop() + if self.auto_close: + fs_obj.close() + diff --git a/fs/patch/patch_builtins.py b/fs/patch/patch_builtins.py new file mode 100644 index 00000000..ffcb06a1 --- /dev/null +++ b/fs/patch/patch_builtins.py @@ -0,0 +1,40 @@ +from .base import patch_method, NotPatched, Patch + + +class PatchBuiltins(Patch): + def __init__(self): + module = globals()["__builtins__"] + super(GlobalsPatch, self).__init__(module) + + @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( + 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..30eaafcb --- /dev/null +++ b/fs/patch/patch_os.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals + +from .base import Patch + + +class OSPatch(Patch): + def __init__(self): + import os + super(OSPatch, self).__init__(os) From 55848e1d8c75a5b1196ba305c35d0af2e2dbb8c2 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 19 Aug 2018 18:15:36 +0100 Subject: [PATCH 2/5] patch stack fixes and logs --- fs/patch/base.py | 14 +++++++------- fs/patch/patch_builtins.py | 4 ++-- fs/patch/patch_os.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/fs/patch/base.py b/fs/patch/base.py index 5b9ca548..5945bec9 100644 --- a/fs/patch/base.py +++ b/fs/patch/base.py @@ -7,7 +7,7 @@ from ..base import FS -logging.getLogger("fs.patch") +log = logging.getLogger("fs.patch") class NotPatched(Exception): @@ -29,16 +29,15 @@ def deco(f): class Patch(object): - def __init__(self, module): - self.module = module - super(Patch, self).__init__() + def get_module(self): + raise NotImplementedError() @property def fs(self): if PatchContext.stack: - return fs[-1].fs_obj + return fs[-1] else: - raise NotPatch() + raise NotPatched() def install(self): for method_name in dir(self): @@ -60,6 +59,7 @@ def __init__(self, fs_obj, auto_close=False): def __enter__(self): self.stack.append(self.fs_obj) + log.debug('%r patched', self.fs_obj) return self def __exit__( @@ -69,6 +69,6 @@ def __exit__( traceback, # type: Optional[TracebackType] ): fs_obj = self.stack.pop() + log.debug('%r un-patched', fs_obj) if self.auto_close: fs_obj.close() - diff --git a/fs/patch/patch_builtins.py b/fs/patch/patch_builtins.py index ffcb06a1..e463b5e4 100644 --- a/fs/patch/patch_builtins.py +++ b/fs/patch/patch_builtins.py @@ -2,9 +2,9 @@ class PatchBuiltins(Patch): - def __init__(self): + def get_module(self): module = globals()["__builtins__"] - super(GlobalsPatch, self).__init__(module) + return module @patch_method() def open( diff --git a/fs/patch/patch_os.py b/fs/patch/patch_os.py index 30eaafcb..7c0ccc81 100644 --- a/fs/patch/patch_os.py +++ b/fs/patch/patch_os.py @@ -4,6 +4,6 @@ class OSPatch(Patch): - def __init__(self): + def get_module(self): import os - super(OSPatch, self).__init__(os) + return os From 29634f760eb527f68a001023b4c89475b6c7003c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 8 Sep 2018 13:41:51 +0100 Subject: [PATCH 3/5] more OS functions --- fs/patch/__main__.py | 24 ++++++++++ fs/patch/_install.py | 10 ++++- fs/patch/_patch.py | 2 + fs/patch/base.py | 89 ++++++++++++++++++++++++++++++++------ fs/patch/patch_builtins.py | 6 +-- fs/patch/patch_os.py | 32 +++++++++++++- 6 files changed, 143 insertions(+), 20 deletions(-) diff --git a/fs/patch/__main__.py b/fs/patch/__main__.py index e69de29b..e3bc7efd 100644 --- a/fs/patch/__main__.py +++ 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 index 2353774d..370231b9 100644 --- a/fs/patch/_install.py +++ b/fs/patch/_install.py @@ -1,6 +1,14 @@ +from .patch_builtins import PatchBuiltins +from .patch_os import PatchOS +installed = False + def install(): """Install patcher.""" - GlobalsPatch().install() + global installed + if not installed: + PatchBuiltins().install() + PatchOS().install() + installed = True diff --git a/fs/patch/_patch.py b/fs/patch/_patch.py index 6184b800..d3e5bc8f 100644 --- a/fs/patch/_patch.py +++ b/fs/patch/_patch.py @@ -2,9 +2,11 @@ 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) diff --git a/fs/patch/base.py b/fs/patch/base.py index 5945bec9..0fda44b3 100644 --- a/fs/patch/base.py +++ b/fs/patch/base.py @@ -1,10 +1,12 @@ 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") @@ -29,37 +31,96 @@ def deco(f): class Patch(object): + stack = [] # type: List[PatchContext] + def get_module(self): raise NotImplementedError() + @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 PatchContext.stack: - return fs[-1] + 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): - for method_name in dir(self): - method = getattr(self, method_name) - _patch = getattr(method, "_fspatch", None) + 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 - original = getattr(self.module, method_name) - _patch["original"] = original - setattr(self.module, method_name, method) + method = getattr(self, method_name) + _patch["original"] = method + + setattr(module, method_name, method) class PatchContext(object): - stack = [] # type: List[FS] 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): - self.stack.append(self.fs_obj) - log.debug('%r patched', self.fs_obj) + Patch.push(self) + log.debug('%r patched', self) return self def __exit__( @@ -68,7 +129,7 @@ def __exit__( exc_value, # type: Optional[BaseException] traceback, # type: Optional[TracebackType] ): - fs_obj = self.stack.pop() - log.debug('%r un-patched', fs_obj) + context = Patch.pop() + log.debug('%r un-patched', context) if self.auto_close: - fs_obj.close() + context.fs_obj.close() diff --git a/fs/patch/patch_builtins.py b/fs/patch/patch_builtins.py index e463b5e4..d5e8ba62 100644 --- a/fs/patch/patch_builtins.py +++ b/fs/patch/patch_builtins.py @@ -3,8 +3,8 @@ class PatchBuiltins(Patch): def get_module(self): - module = globals()["__builtins__"] - return module + import builtins + return builtins @patch_method() def open( @@ -20,7 +20,7 @@ def open( ): try: return self.fs.open( - file, + self.from_cwd(file), mode=mode, buffering=buffering, encoding=encoding, diff --git a/fs/patch/patch_os.py b/fs/patch/patch_os.py index 7c0ccc81..68d9a71d 100644 --- a/fs/patch/patch_os.py +++ b/fs/patch/patch_os.py @@ -1,9 +1,37 @@ from __future__ import unicode_literals -from .base import Patch +import os +from six import PY2 -class OSPatch(Patch): +from .base import original, patch_method, 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 From 8b81346032992a9bfbfbdf11f98525b9bf0f8046 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 8 Sep 2018 13:42:02 +0100 Subject: [PATCH 4/5] translate errors --- fs/patch/translate_errors.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 fs/patch/translate_errors.py 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)) From d55096448e159eb06260a01593c7168950f5f6cf Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 8 Sep 2018 14:07:41 +0100 Subject: [PATCH 5/5] rename method decorator --- fs/patch/base.py | 14 +++++++------- fs/patch/patch_builtins.py | 6 ++++-- fs/patch/patch_os.py | 8 ++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/fs/patch/base.py b/fs/patch/base.py index 0fda44b3..cd9d9c60 100644 --- a/fs/patch/base.py +++ b/fs/patch/base.py @@ -22,13 +22,6 @@ def original(method): return original -def patch_method(): - def deco(f): - f._fspatch = {} - return f - return deco - - class Patch(object): stack = [] # type: List[PatchContext] @@ -36,6 +29,13 @@ class Patch(object): 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) diff --git a/fs/patch/patch_builtins.py b/fs/patch/patch_builtins.py index d5e8ba62..082a3ecb 100644 --- a/fs/patch/patch_builtins.py +++ b/fs/patch/patch_builtins.py @@ -1,12 +1,14 @@ -from .base import patch_method, NotPatched, Patch +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() + @Patch.method() def open( self, file, diff --git a/fs/patch/patch_os.py b/fs/patch/patch_os.py index 68d9a71d..4a55c735 100644 --- a/fs/patch/patch_os.py +++ b/fs/patch/patch_os.py @@ -4,7 +4,7 @@ from six import PY2 -from .base import original, patch_method, Patch +from .base import original, Patch from .translate_errors import raise_os from .. import errors @@ -14,20 +14,20 @@ def get_module(self): import os return os - @patch_method() + @Patch.method() def chdir(self, path): if not self.is_patched: return original(chdir(path)) with raise_os(): return self._chdir(path) - @patch_method() + @Patch.method() def getcwd(self): if not self.is_patched: return original(getcwd)() return self.os_cwd - @patch_method() + @Patch.method() def listdir(self, path): if not self.is_patched: return original(listdir)(path)