about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authortoonn <toonn@toonn.io>2020-07-05 14:35:24 +0200
committertoonn <toonn@toonn.io>2020-07-05 14:35:24 +0200
commita086e977ae6be9e44a372bff2e7a8d5fc59683c3 (patch)
tree520d6146881a469b9d70df5ecbf6f1725df84864
parentad3b41339217b2c7b4981e3e60623e2847b99ca3 (diff)
parentf6fa67646d72b3e1872a9b64633018dde93a698a (diff)
downloadranger-a086e977ae6be9e44a372bff2e7a8d5fc59683c3.tar.gz
Merge branch 'rc_macro_error'
-rw-r--r--ranger/core/actions.py108
-rw-r--r--ranger/ext/macrodict.py81
2 files changed, 121 insertions, 68 deletions
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index fe7d7e5e..42a1d2f5 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -20,21 +20,20 @@ from hashlib import sha512
 from logging import getLogger
 
 import ranger
+from ranger.container.directory import Directory
+from ranger.container.file import File
+from ranger.container.settings import ALLOWED_SETTINGS, ALLOWED_VALUES
+from ranger.core.loader import CommandLoader, CopyLoader
+from ranger.core.shared import FileManagerAware, SettingsAware
+from ranger.core.tab import Tab
 from ranger.ext.direction import Direction
-from ranger.ext.relative_symlink import relative_symlink
 from ranger.ext.keybinding_parser import key_to_string, construct_keybinding
-from ranger.ext.safe_path import get_safe_path
-from ranger.ext.shell_escape import shell_quote
+from ranger.ext.macrodict import MacroDict, MACRO_FAIL, macro_val
 from ranger.ext.next_available_filename import next_available_filename
+from ranger.ext.relative_symlink import relative_symlink
 from ranger.ext.rifle import squash_flags, ASK_COMMAND
-from ranger.core.shared import FileManagerAware, SettingsAware
-from ranger.core.tab import Tab
-from ranger.container.directory import Directory
-from ranger.container.file import File
-from ranger.core.loader import CommandLoader, CopyLoader
-from ranger.container.settings import ALLOWED_SETTINGS, ALLOWED_VALUES
-
-MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>"
+from ranger.ext.safe_path import get_safe_path
+from ranger.ext.shell_escape import shell_quote
 
 LOG = getLogger(__name__)
 
@@ -293,8 +292,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             raise ValueError("Could not apply macros to `%s'" % string)
         return result
 
-    def get_macros(self):  # pylint: disable=too-many-branches,too-many-statements
-        macros = {}
+    def get_macros(self):
+        macros = MacroDict()
 
         macros['rangerdir'] = ranger.RANGERDIR
         if not ranger.args.clean:
@@ -302,57 +301,39 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             macros['datadir'] = self.fm.datapath()
         macros['space'] = ' '
 
-        if self.fm.thisfile:
-            macros['f'] = self.fm.thisfile.relative_path
-        else:
-            macros['f'] = MACRO_FAIL
+        macros['f'] = lambda: self.fm.thisfile.relative_path
 
-        if self.fm.thistab.get_selection:
-            macros['p'] = [os.path.join(self.fm.thisdir.path, fl.relative_path)
-                           for fl in self.fm.thistab.get_selection()]
-            macros['s'] = [fl.relative_path for fl in self.fm.thistab.get_selection()]
-        else:
-            macros['p'] = MACRO_FAIL
-            macros['s'] = MACRO_FAIL
+        macros['p'] = lambda: [os.path.join(self.fm.thisdir.path,
+                                            fl.relative_path) for fl in
+                               self.fm.thistab.get_selection()]
+        macros['s'] = lambda: [fl.relative_path for fl in
+                               self.fm.thistab.get_selection()]
 
-        if self.fm.copy_buffer:
-            macros['c'] = [fl.path for fl in self.fm.copy_buffer]
-        else:
-            macros['c'] = MACRO_FAIL
+        macros['c'] = lambda: [fl.path for fl in self.fm.copy_buffer]
 
-        if self.fm.thisdir.files:
-            macros['t'] = [fl.relative_path for fl in self.fm.thisdir.files
-                           if fl.realpath in self.fm.tags or []]
-        else:
-            macros['t'] = MACRO_FAIL
+        macros['t'] = lambda: [fl.relative_path for fl in
+                               self.fm.thisdir.files if fl.realpath in
+                               self.fm.tags or []]
 
-        if self.fm.thisdir:
-            macros['d'] = self.fm.thisdir.path
-        else:
-            macros['d'] = '.'
+        macros['d'] = macro_val(lambda: self.fm.thisdir.path, fallback='.')
 
         # define d/f/p/s macros for each tab
         for i in range(1, 10):
+            # pylint: disable=cell-var-from-loop
             try:
                 tab = self.fm.tabs[i]
-            except KeyError:
+                tabdir = tab.thisdir
+            except (KeyError, AttributeError):
                 continue
-            tabdir = tab.thisdir
             if not tabdir:
                 continue
             i = str(i)
-            macros[i + 'd'] = tabdir.path
-            if tabdir.get_selection():
-                macros[i + 'p'] = [os.path.join(tabdir.path, fl.relative_path)
-                                   for fl in tabdir.get_selection()]
-                macros[i + 's'] = [fl.path for fl in tabdir.get_selection()]
-            else:
-                macros[i + 'p'] = MACRO_FAIL
-                macros[i + 's'] = MACRO_FAIL
-            if tabdir.pointed_obj:
-                macros[i + 'f'] = tabdir.pointed_obj.path
-            else:
-                macros[i + 'f'] = MACRO_FAIL
+            macros[i + 'd'] = lambda: tabdir.path
+            macros[i + 'p'] = lambda: [os.path.join(tabdir.path,
+                                                    fl.relative_path) for fl in
+                                       tabdir.get_selection()]
+            macros[i + 's'] = lambda: [fl.path for fl in tabdir.get_selection()]
+            macros[i + 'f'] = lambda: tabdir.pointed_obj.path
 
         # define D/F/P/S for the next tab
         found_current_tab = False
@@ -368,25 +349,16 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
                 found_current_tab = True
         if found_current_tab and next_tab is None:
             next_tab = self.fm.tabs[first_tab]
-        next_tab_dir = next_tab.thisdir
+        try:
+            next_tab_dir = next_tab.thisdir
+        except AttributeError:
+            return macros
 
-        if next_tab_dir:
-            macros['D'] = str(next_tab_dir.path)
-            if next_tab.thisfile:
-                macros['F'] = next_tab.thisfile.path
-            else:
-                macros['F'] = MACRO_FAIL
-            if next_tab_dir.get_selection():
-                macros['P'] = [os.path.join(next_tab.path, fl.path)
+        macros['D'] = lambda: str(next_tab_dir.path)
+        macros['F'] = lambda: next_tab.thisfile.path
+        macros['P'] = lambda: [os.path.join(next_tab.path, fl.path)
                                for fl in next_tab.get_selection()]
-                macros['S'] = [fl.path for fl in next_tab.get_selection()]
-            else:
-                macros['P'] = MACRO_FAIL
-                macros['S'] = MACRO_FAIL
-        else:
-            macros['D'] = MACRO_FAIL
-            macros['F'] = MACRO_FAIL
-            macros['S'] = MACRO_FAIL
+        macros['S'] = lambda: [fl.path for fl in next_tab.get_selection()]
 
         return macros
 
diff --git a/ranger/ext/macrodict.py b/ranger/ext/macrodict.py
new file mode 100644
index 00000000..8a8a4a3d
--- /dev/null
+++ b/ranger/ext/macrodict.py
@@ -0,0 +1,81 @@
+import sys
+
+
+MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>"
+
+
+def macro_val(thunk, fallback=MACRO_FAIL):
+    try:
+        return thunk()
+    except AttributeError:
+        return fallback
+
+
+try:
+    from collections.abc import MutableMapping  # pylint: disable=no-name-in-module
+except ImportError:
+    from collections import MutableMapping
+
+
+class MacroDict(MutableMapping):
+    """Mapping that returns a fallback value when thunks error
+
+    Macros can be used in scenarios where several attributes aren't
+    initialized yet. To avoid errors in these cases we have to delay the
+    evaluation of these attributes using ``lambda``s. This
+    ``MutableMapping`` evaluates these thunks before returning them
+    replacing them with a fallback value if necessary.
+
+    For convenience it also catches ``TypeError`` so you can store
+    non-callable values without thunking.
+
+    >>> m = MacroDict()
+    >>> o = type("", (object,), {})()
+    >>> o.existing_attribute = "I exist!"
+
+    >>> m['a'] = "plain value"
+    >>> m['b'] = lambda: o.non_existent_attribute
+    >>> m['c'] = lambda: o.existing_attribute
+
+    >>> m['a']
+    'plain value'
+    >>> m['b']
+    '<\\x01\\x01MACRO_HAS_NO_VALUE\\x01\\x01>'
+    >>> m['c']
+    'I exist!'
+    """
+
+    def __init__(self, *args, **kwargs):
+        super(MacroDict, self).__init__()
+        self.__dict__.update(*args, **kwargs)
+
+    def __setitem__(self, key, value):
+        try:
+            real_val = value()
+            if real_val is None:
+                real_val = MACRO_FAIL
+        except AttributeError:
+            real_val = MACRO_FAIL
+        except TypeError:
+            real_val = value
+        self.__dict__[key] = real_val
+
+    def __getitem__(self, key):
+        return self.__dict__[key]
+
+    def __delitem__(self, key):
+        del self.__dict__[key]
+
+    def __iter__(self):
+        return iter(self.__dict__)
+
+    def __len__(self):
+        return len(self.__dict__)
+
+    def __str__(self):
+        return str(self.__dict__)
+
+
+if __name__ == '__main__':
+    import doctest
+    sys.exit(doctest.testmod()[0])