This post discusses discusses problems with the @g.command decorator.  
Later sections discuss solutions. These later sections are ENB (Engineering 
Note Book) discussions, which can safely be ignored by all except the usual 
suspects...

Promises to do something "today" don't always get kept because a seemingly 
simple task like adding an insert-file-name command becomes unbearable.  
Creating a better way to define commands has become urgent.

===== Background

@g.command has its uses, especially in plugins.  It allows any code to 
define a new Leo command without having any access to Leo's core code.  For 
this reason alone, the @g.command decorator is here to stay.

However, using @g.command to add Leo commands is often *way* too clumsy.  
There are several problems using @g.command.  To see these problems 
clearly, let's look at a typical example, from leoVim.py::

    @g.command(':gt')
    def vim_gt(event):
        '''cycle-focus'''
        c = event.get('c')
        if c and c.vimCommands:
            c.vimCommands.cycle_focus()

This code sucks, for at least the following reasons:

1. It requires an outer-level function-name (vim_gt) that has no value 
except to the @g.command decorator itself.  This is useless verbiage and 
just clutters the namespace.  Worse, although the decorator doesn't require 
distinct names, disabling a pylint check for duplicate names would be way 
too dangerous.

2. It repeats the template code for each different command.  This is pure 
cruft. Worse, the tests for c and c.vimCommands would not be necessary in a 
subcommander.

3. It is usually bad design to define the command at the top level.  
Commands should be defined in the classes that contain their code!

For all these reasons, vim-mode commands clearly should *not* be defined 
using the @g.command decorator.  The question is, how to define them 
cleanly?

Everything from here on is an ENB entry.  Feel free to ignore, unless you 
are Terry ;-)

===== Motivation

It would be possible to define vc commands in vc.finishCreate as is done by 
sub-commanders in leoEditCommands.py, but as I was thinking about this 
problem I discovered how to simplify the definition of almost *all* of 
Leo's commands.  This is too good to ignore, for the following reasons:

1. The simplifications to be described reduce order constraints during 
startup.  Any such reduction is, all by itself, extremely valuable, because 
such order constraints are the main complicating factor in the startup code.

2. As always, simplifications to complex code (and Leo's startup code is 
complex in hard-to-see ways) promise further simplifications.  No further 
simplifications are apparent *now*, but it would be bad practice not to 
simplify the code when I can.

===== Details

At present, there are several ways (not sure how to count them ;-) to 
define large numbers of commands.  By defining a command I mean associating 
a string (the command's name) with a method of some class.

Although details vary, the basic idea in each case is to define a 
**commands dict**, a Python dict whose keys are command names (Python 
strings) and whose values are methods.

In particular, each subcommander in leoEditCommands.py defines a 
getPublicCommands method that returns a commands dict.

ECM.finishCreateEditCommanders then merges all the commands dicts, like 
this::

    def finishCreateEditCommanders (self):
        '''Finish creating edit classes in the commander.
        Return the commands dictionary for all the classes.'''
        c,d = self.c,{}
        for name, theClass in self.classesList:
            theInstance = getattr(c,name)
            theInstance.finishCreate()
            theInstance.init()
            d2 = theInstance.getPublicCommands()
            if d2:
                d.update(d2)
        return d

But none of this should be necessary.  Or rather, it should happen in a 
different place, and at no *particular* time.

===== A much better way to define commands.

The basic problem with the code above is that it must happen at the exact 
instant that various finishCreate methods get called.  Yes, the present 
code works, but it depends *far* too much on code order during startup.

We can remove *all* these unnecessary ordering constraints as follows:

1.  Define init phase 0: create ivars.

This phase is trivial, for our present purposes.  Commands.__init__ will 
just define c.commandsDict and its inverse, as *empty* Python dictionaries.

At present, the inverse of c.commandsDict is c.k.inverseCommandsDict.  How 
stupid is that?  It must be a c (Commands) ivar, not a c.k 
(KeyHandlerClass) ivar! Doh. Using a c.k ivar means that c.k must be inited 
*before* c.k.inverseCommandsDict is ever used.  This is a completely 
unnecessary constraint.

In short, the inverseCommandsDict must be a Commands ivar, not a 
KeyHandlerClass ivar.  This will ensure that c.inverseCommandsDict will 
exist before any subcommander's *ctor* gets called.

2. Define c.registerCommands (plural).  This will call a new method, 
c.registerCommand, for each item in a commands dict.

Important: c.k.registerCommand must remain for compatibility, but it will 
probably just call c.registerCommand.

With just these simple changes, *any* class can call c.registerCommands at 
*any* time.  In particular, any class can call c.registerCommands in its 
ctor, or its ctor's helpers.  There is no need to wait until finishCreate 
gets called!

Defining command names in the ctor logic may leave some finishCreate 
methods empty.  If so, we can rejoice--another ordering constraint has 
disappeared forever!

===== Testing

I expect the changeover to the new scheme to be straightforward.  It may 
happen "today" (TM).

This new scheme should be safe, but just to make sure, I'll probably enable 
the new code with g.NEW_COMMANDS.

It's not clear how well the present unit tests actually tests the 
association between commands and their names.  A separate unit test may be 
advisable.

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 post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/leo-editor.
For more options, visit https://groups.google.com/d/optout.

Reply via email to