From 2c33cedaaac2a21d538f20fcb6fc36e23467a79d Mon Sep 17 00:00:00 2001 From: toonn Date: Thu, 18 Jul 2019 16:26:18 +0200 Subject: Catch exceptions when resolving macros in rc.conf While parsing the settings in `rc.conf` `self.fm.thisfile` doesn't exist yet. --- ranger/core/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 435fcf13..00394be5 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -303,9 +303,9 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m macros['datadir'] = self.fm.datapath() macros['space'] = ' ' - if self.fm.thisfile: + try: macros['f'] = self.fm.thisfile.relative_path - else: + except AttributeError: macros['f'] = MACRO_FAIL if self.fm.thistab.get_selection: -- cgit 1.4.1-2-gfad0 From f6fa67646d72b3e1872a9b64633018dde93a698a Mon Sep 17 00:00:00 2001 From: toonn Date: Thu, 26 Sep 2019 11:47:39 +0200 Subject: 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. --- ranger/core/actions.py | 106 ++++++++++++++++++------------------------------ ranger/ext/macrodict.py | 81 ++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 67 deletions(-) create mode 100644 ranger/ext/macrodict.py 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]) -- cgit 1.4.1-2-gfad0