Skip to content

Extensions

Maurice Lam edited this page Nov 20, 2019 · 9 revisions

Hashbang uses extensions to extend the functionality of your commands. An extension is specified as a parameter of @command and modifies the given HashbangCommand to extend the behavior. A commonly used extension is Argument, which changes how the arguments are applied.

Using an extension

Extensions are listed as parameters of @command. For example, suppose we want to use the Argument extension, and our hypothetical Version extension, the code will look like this:

@command(
  Argument('trailing_newline', aliases=('n',)),
  Version('1.0.1'))
def echo(*message, trailing_newline=True):
  print(' '.join(message), end=('\n' if trailing_newline else ''))

Implementing an extension

To see how extensions can be implemented, we can look at the implementation of Argument.

class Argument:
  def __init__(self, name, *, **kwargs):
    self.name = name
    [...]

  def apply_hashbang_extension(self, cmd):
    cmd.arguments[self.name] = (cmd.signature.parameters[self.name], self)

As you can see, an extension is just a regular object implementing the apply_hashbang_extension method. In that method the HashbangCommand class is passed in, and the extension would modify the command object to achieve the desired behavior. In the case of Argument, it replaces itself in cmd.arguments with the extension itself, thus allowing additional attributes to be defined.

Extending Argument

A common use case for extensions is to extend Argument. Since Argument is responsible for adding arguments to the parser, extending Argument allows it to add additional arguments or modify behavior of an existing argument. In our Version example above, we can add a version argument so that users can see the version number using --version.

class VersionArgument(Argument):
  def __init__(self, version):
    super().__init__()
    self.version = version

  def add_argument(self, cmd, arg_container, param):
    arg_container.add_argument(
      '--version',
      action='version',
      version=self.version)

By overriding add_argument and adding a --version argument, any invocation with --version will print the version number and exit immediately. The arg_container argument passed to the add_argument function is a parser-like object from the argparse module. To plug this custom Argument into the extension, we will implement Version as follows:

class Version:
  def __init__(self, version):
    self.version_arg = VersionArgument(version)

  def apply_hashbang_extension(self, cmd):
    cmd.arguments['version'] = (None, self.version_arg)

This would add the VersionArgument into the parser. HashbangCommand.arguments, is a dictionary of {name: (param, argument)}. param is the parameter definition from inspect.signature(). But since this argument doesn't map to any parameter in the decorated function, we can just pass None. argument is the argument object, which we will use the VersionArgument class we implemented above.

In fact, VersionArgument and Version can be combined into the same class, so it would just pass self in the arguments dict. You can see a complete version of that below, or at tests/extension/version.py

Complete Version extension
class Version(Argument):
  def __init__(self, version):
    super().__init__()
    self.version = version

  def apply_hashbang_extension(self, cmd):
    cmd.arguments['version'] = (None, self)

  def add_argument(self, cmd, arg_container, param):
    arg_container.add_argument(
        '--version',
        action='version',
        version=self.version)
Clone this wiki locally