For the last few days I have been working on #325 <https://github.com/leo-editor/leo-editor/issues/325>. The first comment of this issue starts with:
> I want to replace @cmd decorators by by @g.command decorators everywhere, thereby eliminating g.cmd_instance_dict, which is unbearably ugly. *The puzzle* Following this stack overflow post <https://stackoverflow.com/questions/11731136/> re "self" arguments in decorators, I created the following prototype: global_commands_dict = {} def command_method(name): """A decorator that creates a Leo command from a method.""" def _wrapper(f): """This wrapper is always called with one argument: the wrapped function.""" def inner_wrapper(self, event): """Inner wrapper docstring.""" return f(self, event) inner_wrapper.__name__ = f.__name__ # Doesn't work. inner_wrapper.__doc__ = f.__doc__ # works. return inner_wrapper global_commands_dict[name] = _wrapper return _wrapper class Test: @command_method('get') def get(self, event): """docstring for Test.get.""" g.trace(Test.get.__doc__) event = g.Bunch(c=c) Test().get(event) g.printObj(global_commands_dict, tag='dict') Running this script gives the following output. get docstring for Test.get. dict: { get:<function command_method.<locals>._wrapper at 0x00000170F5D90288> } Alas, I have not been able to generalize this *apparently* successful prototype so that it works in Leo. There is another possible way forward. See below. *Background* You would think that defining decorators would be easy. pep 318 <https://www.python.org/dev/peps/pep-0318/> defines the effect of decorators in the current syntax section <https://www.python.org/dev/peps/pep-0318/#current-syntax>: @dec2 @dec1 def func(arg1, arg2, ...): pass is equivalent to: def func(arg1, arg2, ...): pass func = dec2(dec1(func)) without the intermediate assignment to the variable func. This seems straightforward, but binding issues are enough to make my head explode :-) The argument to any decorator is the method to be decorated. For decorated methods, this an* unbound* method. The "self" argument is available, but only later, when the *bound* method gets called. There are also (to me) confounding issues about what the argument lists to the _wrapper and inner_wrapper functions should be. *A failed g.command_name function* I defined g.command_method as follows: def command_method(name): def _wrapper(f): """f is the wrapped function.""" def inner_wrapper(self, event=None, *args, **kwargs): """Inner wrapper docstring.""" return f(self, event, *args, **kwargs) # inner_wrapper.__name__ = f.__name__ # happens too late to be useful. f.is_command = True f.command_name = name inner_wrapper.__doc__ = f.__doc__ # works. return inner_wrapper global_commands_dict[name] = _wrapper return _wrapper Alas, this does not work. Using this to define the undo/redo commands does create undo and redo commands, but the inner wrapper is never called. In contrast, defining undo like this does work: @g.command('undo') def undo_command(event): c = event.get('c') if not c: return c.undoer.undo(event) *Another possibility* Leo already contains per-class and per-file @cmd decorators. At present these cmd functions use g.new_cmd_decorator. For example, LeoBody.cmd is: def cmd(name): """Command decorator for the c.frame.body class.""" # pylint: disable=no-self-argument return g.new_cmd_decorator(name, ['c', 'frame', 'body']) Another possible solution would be to create per-class cmd methods that use "self" to avoid call g.new_cmd_decorator. Iirc, I tried and failed to do this years ago. It would be best if the new cmd methods were relatively simple :-) In any case, they must not use g.cmd_instance_dict, either directly or indirectly. *The commands and commands2 branches* The commands branch avoids these puzzles by creating top-level *function* that define all of Leo's commands. I have been using a script to do the dirty work. The commands2 branch defines the undo and redo commands using top-level methods. Replacing these by @g.command_method decorators applied to the undo/redo methods themselves fails. The commands are not functional. As a result various unit tests fails. *Summary* Your challenge, should you choose to accept it, is to create g.command_method function in leoGlobals.py which can be used in any file as a decorator. Alternatively, you may create a *relatively* simple template for the various cmd functions/methods. No solution may use g.cmd_instance_dict, either directly or indirectly. All unit tests must pass :-) The commands2 branch contains failed experiments. I'll use the code in the commands branch if no reasonable solution to these puzzles can be found. There is no great hurry. I won't merge "commands" into devel any time soon. Edward -- You received this message because you are subscribed to the Google Groups "leo-editor" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion on the web visit https://groups.google.com/d/msgid/leo-editor/a7603bef-3d74-4b12-b1ae-6da052ab2a39%40googlegroups.com.
