Changing command tables

Gregory Szorc gregory.szorc at gmail.com
Sat Apr 19 01:27:43 CDT 2014


I would like to propose changing how command tables work.

Current State
=============

Extensions/Python modules contain a specially named variable called
"cmdtable." It is a dict mapping the command name to a tuple of 2 *or* 3
elements.

Extensions typically do something like:

cmdtable = {}
command = cmdutil.command(cmdtable)

Putting @command on a function magically updates the cmdtable dict for
the local module.

mod.cmdutil is consulted at dispatch time along with some other
variables (such as commands.norepo) to determine how an individual
command works.

Problems with Current
=====================

The fact values of cmdtable instances are either 2-tuples or 3-tuples is
a giant wart. IMO the values should be uniform.

The fact values of cmdtable are tuples makes extending command entries
difficult. Say I want to record some extra per-command metadata.
Appending positional values to the tuples is no fun. It doesn't produce
readable code (if entry[5]...) and it isn't extensible. Continuing to
add special variables like commands.norepo and cmdutil.unfinishedstates
for command-specific metadata is confusing and has and will continue to
lead to harder-to-maintain code. (I am writing this proposal because I
ran into a lot of bad solutions for adding per-command metadata and
figured I might as well propose a good longer-term solution.)

It's not entirely clear to me why cmdtable needs to be defined in each
extension/module. Why can't @command register entries with a registrar
elsewhere (perhaps somewhere in cmdutil)? If we're trying to associate
commands with certain extensions, there's Python magic we can employ
(such as looking at __file__, __module__, __class__, etc of the wrapped
callable). What I'm trying to say is that I'm pretty sure we could
eliminate cmdtable altogether if we wanted to.

Proposal
========

I would like to convert "command table" entries to have a dict
somewhere. Instead of (func, options, [synopsis]), how about (func,
options, extra) or even (func, options, synopsis, extra) if we really
care about backwards compatibility. "extra" here is a dict of optional
attributes describing per-command metadata. Some values we could put in
that dict include:

* The synopsis (currently passed to @command)
* Whether it's a "no repo" command (currently this is space delimited
string in commands.py - hacky)
* Whether it's an "optional repo" command (...)
* Whether it's an "infer repo" command (...)
* Unfinished state info (currently in cmdutil.unfinishedstates)
* How to resume/abort an unfinished command (useful for telling users
what to run when |resolve| completes; also enables automagical |hg
continue| for doing this automatically)
* Probably some other things I'm not thinking of

The end goal is to have a single data structure holding pretty much all
the per-command metadata. No more scattering of command behavior across
multiple modules and data structures. Just a single, clean, scalable dict.

As this new command table gained features, we would of course add
optional named arguments to @command. e.g.

@command('^clone', [...],  _('[OPTION]... SOURCE [DEST]'), norepo=True)

(note the addition of "norepo=True")

I think this approach is relatively non-controversial... if we were
starting from a clean slate. The hard part is deciding what to do about
backwards compatibility.

Idea #1
-------

Modify the existing cmdutil.command decorator to in the absence of an
argument or in the presence of a non-dict argument to do something else
with the registered commands. e.g. we would establish cmdutil.cmdtable
for the new, unified data structure and put everything there. No more
per-module cmdtable. Just a central instance.

Dispatching would merge both old and new data structures. Extensions
would continue working without any required modifications.
commands.norepo, etc would still work (but would be deprecated in favor
of arguments to the @command decorator).

Downside: Writing dual compatible extensions is annoying. You probably
have to fall back to "decorators are just function wrappers" and do
something like:

def mycommand(ui, *args, **kwargs):
    """The body of hg mycommand."""

try:
    mycommand = command(mycommand, '^mycommand', [...], _('hg
mycommand'), norepo=True)
except Exception:
    mycommand = cmdutil.command(cmdtable)(mycommand, '^mycommand',
[...], _('hg mycommand'))
    commands.norepo += ' mycommand'

That's yucky.

Idea #2
-------

In this variant of 1, we establish a new @command decorator that has the
new behavior while preserving the existing decorator.

This makes extension compatibility a little easier. We can probably
employ some magic whereby the following does the right thing:

@command2(norepo=True)
@command('^mycommand', [...], _('hg mycommand'))
def mycommand(ui, *args, **kwargs)
    pass

If you only wanted to be compatible with the new world order, you'd do:

@command2('^mycommand', [...], _('hg mycommand'), norepo=True)
def mycommand(ui, *args, **kwargs)
    pass

Only 3rd party extensions would continue to use the old, deprecated
decorator.

Idea #3
-------

Remove the cmdtable argument from cmdutil.command(), remove
commands.norepo, etc.

Downside: Extensions break. (I think this is a non-starter.)

Idea #4
-------

Do nothing. Keep adding one-off variables everywhere for tracking each
new per-command metadata. Make commands harder to implement because
their behavior is governed in multiple, non-obvious ways (as opposed to
a unified decorator).

Misc
====

I know I didn't go into details about eliminating per-file cmdtable
dicts. It's an idea that could be done with relative ease if we
committed to changing how command registration worked. I just wanted to
float the idea out there.

Thoughts? (I'm tentatively signing myself up for the legwork.)

Gregory


More information about the Mercurial-devel mailing list