diff options
author | toonn <toonn@toonn.io> | 2019-09-26 11:47:39 +0200 |
---|---|---|
committer | toonn <toonn@toonn.io> | 2019-09-27 14:42:45 +0200 |
commit | f6fa67646d72b3e1872a9b64633018dde93a698a (patch) | |
tree | d3a483bf1a87cb4a72b26474150c29a8ae80914a | |
parent | 2c33cedaaac2a21d538f20fcb6fc36e23467a79d (diff) | |
download | ranger-f6fa67646d72b3e1872a9b64633018dde93a698a.tar.gz |
Add MacroDict
Macros are resolved for set commands. A contributor wanted to add a setting for a date format string to be used in ranger's status bar. The string they tried happened to include `%d` which is a valid ranger macro for the name of `thisdir`. This attribute is not yet defined when ranger is reading `rc.conf` and this uncaught exception would crash ranger. To work around this we need to catch `AttributeError`s for values that might not exist yet. To avoid the repetition that'd come with all these try-catch statements that behavior has been encapsulated in a new MacroDict. This is almost as easy to use as the regular python dictionary used previously. The only difference being that we need to wrap values that might cause problems in a python `lambda`, though it doesn't do any harm to wrap values that can't raise exceptions.
-rw-r--r-- | ranger/core/actions.py | 106 | ||||
-rw-r--r-- | ranger/ext/macrodict.py | 81 |
2 files changed, 120 insertions, 67 deletions
diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 00394be5..2c7f3b9a 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -21,21 +21,20 @@ from sys import version_info 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.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 - +from ranger.ext.shell_escape import shell_quote -MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>" LOG = getLogger(__name__) @@ -294,8 +293,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: @@ -303,57 +302,39 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m macros['datadir'] = self.fm.datapath() macros['space'] = ' ' - try: - macros['f'] = self.fm.thisfile.relative_path - except AttributeError: - 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 @@ -369,25 +350,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]) |