CommandTreeBuilder

Summary

Ingredient for arranging all the Command classes into a tree of objects.

Description

The command tree builder ingredient is a part of the command recipe. It is responsible for instantiating all sub-commands and arranging them into a tree for other ingredients to work with (most notably the argument parser ingredient).

The secondary task is to add the spices requested by the top-level command to the bowl. This lets other ingredients act differently and effectively allows the top-level command to influence the runtime behavior of the whole recipe.

Spices

This ingredient is not influenced by any spices.

Context

This ingredient adds two objects to the context:

cmd_tree
A tree of tuples that describes all of the commands and their sub commands.
cmd_toplevel
The top-level command object.

In addition, this ingredient inspects the spieces required by the top-level command and adds them to the bowl.

Command Line Arguments

This ingredient is not exposing any command line arguments.

Examples

Let’s create two examples below. One for a simple command and another for a hierarchical command. This example will not use the full command recipe, to focus on the side effects of just the command tree builder ingredient.

Flat Command

We’ll need a command object:

>>> from guacamole.recipes.cmd import Command
>>> class HelloWorld(Command):
...     pass

Note that the tree builder is called with an instance of the command, not the class. This allows the top-level command to have a custom initializer, which might be helpful.

>>> from guacamole.core import Context
>>> from guacamole.ingredients import cmdtree
>>> ctx = Context()
>>> cmdtree.CommandTreeBuilder(HelloWorld()).added(ctx)

The context now has the cmd_toplevel object which is just the instance of the command we’ve used.

>>> ctx.cmd_toplevel
<HelloWorld>

Similarly, we’ll have a tree of all the commands and their names in cmd_tree:

>>> ctx.cmd_tree
cmd_tree_node(cmd_name=None, cmd_obj=<HelloWorld>, children=())

The first element of the tuple is the effective command name. This can be used to rename a sub-command. Note that typically the command.name attribute is used (see get_cmd_name()). The second element is the instance and the last element is a tuple of identical cmd_tree_node tuples, one for each of the sub-commands. We’ll see how that looks like in the next example.

Nested Commands

We’ll need a few commands for this example. Let’s replicate the git, git commit, git stash, git stash pop and git stash list commands.

>>> from guacamole.recipes.cmd import Command
>>> class StashList(Command):
...     pass
>>> class StashPop(Command):
...     pass
>>> class Stash(Command):
...     sub_commands = (('list', StashList), ('pop', StashPop))
>>> class Commit(Command):
...     pass
>>> class Git(Command):
...     sub_commands = (('commit', Commit), ('stash', Stash))

Now, let’s feed the Git class to the context.

>>> from guacamole.core import Context
>>> from guacamole.ingredients import cmdtree
>>> ctx = Context()
>>> cmdtree.CommandTreeBuilder(Git()).added(ctx)

The cmd_toplevel is as before (the Git instance). Let’s look at the more interesting command tree.

>>> ctx.cmd_tree
cmd_tree_node(cmd_name=None, cmd_obj=<Git>,
    children=(cmd_tree_node(cmd_name='commit', cmd_obj=<Commit>,
    children=()), cmd_tree_node(cmd_name='stash',
    cmd_obj=<Stash>, children=(cmd_tree_node(cmd_name='list',
    cmd_obj=<StashList>, children=()), cmd_tree_node(cmd_name='pop',
    cmd_obj=<StashPop>, children=())))))

Blah, that’s mouthful. Let’s see particular fragments to understand it better.

>>> ctx.cmd_tree.children[0].cmd_name
'commit'
>>> ctx.cmd_tree.children[1].cmd_name
'stash'
>>> ctx.cmd_tree.children[1].children[0].cmd_name
'list'
>>> ctx.cmd_tree.children[1].children[1].cmd_name
'pop'

Most of the time you won’t have to use this data. Typically, it is consumed by the argument parser ingredient. Still, if you need it, here it is.

CommandTreeDispatcher

Summary

Ingredient for executing the invoked() methods of all the commands that were selected by the user on command line.

Description

This ingredient is responsible for invoking commands. It works during the dispatch phase of the application life-cycle. Since earlier stages can be interrupted it is not aways reached. E.g. when the application is invoked with the --help argument.

The way this ingredient works is simple. It assumes that the argument parser creates a specific structure of references to command objects. The structure is stored in the argparse name-space object (which is available in ctx.args after the parsing phase. The structure is a sequence of attributes ctx.args.command0, ctx.args.command1, ctx.args.command2, etc. The first one, ctx.args.command0 is always present. Subsequent attributes are present if sub-commands are specified on the command line. For example, keeping our git sample in mind, the following command:

$ git stash

Will result in ctx.args.command0 instance of the Git command and ctx.args.command1 an instance of the GitStash command. The dispatcher ingredient will invoke the command0, look at the return value and then (most likely) proceed to command1 (N+1 in general).

The way return value is interpreted is interesting. In general, there are three cases:

  • None is interpreted as “nothing special happened”. In the example above. The git stash will first call Git.invoked(), see the (default) None and will proceed to call GitStash.invoked().
  • A generator is interpreted as a context-manager like. This allows, for example, the git command to use a context manager in its invoked() method to provide some managed resource to each sub-command. Note that the invoked method must behave as it if was decorated with @functools.contextmanager but it must not be actually decorated like that.
  • Any other return value is interpreted as an error code and stops recursive command dispatch. It will be finally returned from the main() method or raised as a SystemExit exception.

Spices

This ingredient is not influenced by any spices.

Context

This ingredient does not change the context. It does depend on the args object that is published by the argument parser ingredient.

Command Line Arguments

This ingredient is not exposing any command line arguments.

Examples

Let’s see how command invocation works in the few specific examples below.

Single Command

Let’s start with a hello-world command first:

>>> from guacamole.recipes.cmd import Command
>>> class HelloWorld(Command):
...     def invoked(self, ctx):
...         print("Hello World")

Let’s create the necessary infrastructure for using the dispatcher:

>>> import argparse
>>> from guacamole.core import Context
>>> from guacamole.ingredients import cmdtree
>>> ctx = Context()
>>> ctx.args = argparse.Namespace()

Now let’s run the HelloWorld command:

>>> ctx.args.command0 = HelloWorld()
>>> cmdtree.CommandTreeDispatcher().dispatch(ctx)
Hello World

Success! The print worked and we also got the exit code (None, which is not printed by the repl).

Next, let’s implement the classic UNIX false(1) command:

>>> class false(Command):
...     def invoked(self, ctx):
...         return 1

Now, let’s invoke it:

>>> ctx.args.command0 = false()
>>> cmdtree.CommandTreeDispatcher().dispatch(ctx)
1

One. Also good.

All command line tools return an exit code. If you actually run this command in the shell you can inspect the return code in several ways (depending on what is your shell). On Windows that is:

echo %ERRORLEVEL%

And on all other systems, that are mostly using Bash by default:

echo $?

In both cases, you should see 1 being printed by those echo statements.

Nested Commands

Let’s expand the Git example to examine the context-manager-like behavior.

>>> class GitLibrary(object):
...     def __enter__(self):
...         print("Git initialized")
...         return self
...     def __exit__(self, *args):
...         print("Git finalized")
...     def commit(self):
...         print("Using git to commit")

>>> class Commit(Command):
...     def invoked(self, ctx):
...         with GitLibrary() as git:
...             git.commit()

>>> class Git(Command):
...     sub_commands = (('commit', Commit),)

Now, let’s see what dispatch does here:

>>> ctx.args.command0 = Git()
>>> ctx.args.command1 = Commit()
>>> cmdtree.CommandTreeDispatcher().dispatch(ctx)
Git initialized
Using git to commit
Git finalized

If you have many commands that need to use some shared resource, you may be tempted to move the initialization to a shared code path. Guacamole allows you to do this by calling all the invoked() methods of all of the commands specified on command line.

Let’s modify the example to show this. The git library code will say as-is. The commit and git commands will be changed, to move the initialization code around.

>>> class Commit(Command):
...     def invoked(self, ctx):
...         ctx.git.commit()

>>> class Git(Command):
...     sub_commands = (('commit', Commit),)
...     def invoked(self, ctx):
...         with GitLibrary() as git:
...             ctx.git = git
...             yield

Now, let’s see what dispatch does now:

>>> ctx.args.command0 = Git()
>>> ctx.args.command1 = Commit()
>>> cmdtree.CommandTreeDispatcher().dispatch(ctx)
Git initialized
Using git to commit
Git finalized

No change, that’s running exactly as before but now we can add more commands without duplicating the relevant code over and over.

Note

Here, the finalization will happen even if something bad happens (e.g. Commit raising an exception). It’s not useful often but it can be a way to use the context manager protocol with commands.