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.

Reply via email to