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.