Custom commands

The command management module allows you to extend the functionality of your framework by adding commands that are common to all your projects or unique to one. Socon integrates three kind of commands:

  • BaseCommand that we call general command. These commands are considered global as they don’t require to load a ProjectConfig. They are essentially used to execute general tasks that are not project related.

  • ProjectCommand inherit from BaseCommand. These commands require a project configuration to work. For this command to work you will need to specify the --project option.

  • Subcommand inherit from BaseCommand. By subclassing your command from this class, you can create a command that has it’s own list of ProjectCommand and BaseCommand.

Register your commands

To register a command in your framework you simply need to add a management/commands directory in one of your projects or in the common space. Socon will register every command in each module in that directory:

myframework/
    myframework/
        __init__.py
        management/
            __init__.py
            settings.py
            commands/
                __init__.py
                launch.py
                publish.py
    projects/
        __init__.py
        apollo/
            __init__.py
            projects.py
            management/
                __init__.py
                commands/
                    __init__.py
                    launch.py
                config.py
        artemis/
            __init__.py
            projects.py
    manage.py

In this example, we have created multiple modules in both the common space and in the apollo project under the management/commands directory. In each of these modules there is a class that describe the command that we want to register in the manage.py. Each of these commands inherit from either the BaseCommand or the ProjectCommand.

Project command

Let’s take a close look at the launch.py module that is defined in the common space. The launch command defined in that module will be a ProjectCommand command and will be made available for any project in your framework.

Important

Because the command is declared as a ProjectCommand and in the common space, it is available for any project that exist or that will be created.

from socon.core.management.base import ProjectCommand, Config
from socon.core.registry.base import ProjectConfig


class LaunchCommand(ProjectCommand):
    name = 'launch'

    def handle(self, config: Config, project_config: ProjectConfig):
        spacecraft = project_config.get_setting('SPACECRAFT')
        print(f'Launching the {spacecraft} SpaceCraft to the moon')

The launch command being a ProjectCommand, it must be called using the --project option, because ProjectCommand must load a project configuration to work:

If we execute this command, and if we define the SPACECRAFT project setting as Saturn IB it will output:

Launching the Saturn IB SpaceCraft to the moon

ProjectCommand is really powerful and allows you to make generic commands for each of your projects if it’s well defined.

Overriding project commands

Commands are based on managers and hooks. Socon registers the built-in commands and then searches for commands in Socon, the INSTALLED_PLUGINS, the common space, and finally the INSTALLED_PROJECTS.

During the search, each command are registered in the CommandManager and saved alongside its config object. When Socon looks for a command it will proceed as follows:

  1. Search in Socon config for built-in commands.

    If the command is found save it.

  2. Search in the plugins.

    If the command is found, override the previous command.

  3. Search in the common space config for commands.

    If the command is found, override the previous command.

  4. Did the user pass the --project?

    Yes, if the command is found in the project, it overrides the previous command. No, return the last command found

To override a command, the new command must have the same name. Let’s take an example to illustrate what we just said. In the above tree structure, we have created a apollo project that defines a launch.py module as well. Let’s take a look at what is inside:

from socon.core.management.base import ProjectCommand, Config
from myframework.management.commands.launch import LaunchCommand


class LaunchCommand(LaunchCommand):
    name = 'launch'

    def handle(self, config: Config, project_config: ProjectConfig):
        spacecraft = project_config.get_setting('SPACECRAFT')
        self.prepare(spacecraft)
        super().handle(config, project_config)

    def prepare(self, spacecraft):
        print(f"Specific things to do for the launch of {spacecraft}")

As you can notice, the command has the same name as the one we declared earlier. It also inherits from the one in the common space. This command will do the exact same thing as the previous, one but it will add a new function that will prepare the spacecraft before being launched. We also need to specify the SPACECRAFT variable in the apollo project config. For this example, we will define it as Orion.

If we start the command, with:

python manage.py launch --project apollo

We would have the following output:

Specific things to do for the launch of Orion
Launching the Orion SpaceCraft to the moon

Important

This overrides only the launch command of the apollo project (as it’s the only one that redefines it). If we start the same command but with the artemis project, we would get the result previously shown.

Management commands from plugin that have been unintentionally overridden can be made available under a new name by creating a new command in one of your projects or in the common space.

Limiting scope

You can limit the access to a ProjectCommand by using the projects attribute. Using this attribute, you restrict the access to this command.

from socon.core.management.base import ProjectCommand


class SimpleCommand(ProjectCommand):
    name = 'simple_command'

    # limit the scrope to 2 projects
    projects = ['apollo', 'artemis']

    def handle(...):
        # ...

General commands

Let’s take a closer look at the publish.py module that is defined in the common space. The publish command defined in that module will be a BaseCommand command and will be made available as a general command (as we like to call it).

..note:

This kind of command can also be declared in projects even if it does not
make a lot of sense. It's important to mention that even if you do declare
this command in a project, it will still be shown as as general command and
will not be binded to the project.
from socon.core.management.base import BaseCommand, Config


class PublishCommand(BaseCommand):
    name = 'publish'

    def handle(self, config: Config):
        print(f'Publishing an article about NASA')

That is it, pretty simple! We use the BaseCommand class and we give it a name. Then, this command will be available in the manage.py.

Running the command:

python manage.py publish

Would give as you all guessed:

Publishing an article about NASA

Overriding general commands

General command acts the same as project command. This means that you can override a general command in the common space, a plugin or a project.

If you create a general command in a project with the same name as one in the common space for example, you can use it by calling your command with the --project option. If the command exist it will be executed, otherwise an error will be thrown as Socon expects the command to exist in the project. There is no fallback.

You can ask yourself, why you would override a general command? Let’s take an example where you want to redefine the built-in createproject in your framework because you want to improve it. You just have to create the createproject command inside the common space and it will be used instead of the general command the next time you call it.

Accepting optional arguments

All commands can be easily modified by accepting additional command line options. These custom options can be added in the add_arguments() method like this:

class LaunchCommand(ProjectCommand):
    name = 'launch'

    def add_arguments(self, parser: ArgumentParser) -> None:
        parser.add_argument('--countdown', help=(
            "Countdown before we launch the rocket"
        ), default=0)

    def handle(self, config: Config, project_config: ProjectConfig):
        spacecraft = project_config.get_setting('SPACECRAFT')
        countdown = config.getoption('countdown')
        for i in range(int(countdown), 0, -1):
            print(i)
            sleep(1)
        print(f'Launching the {spacecraft} SpaceCraft to the moon')

As you can see in this example, we have extended the functionality of our command by adding a countdown before launching our spacecraft.

We can now call this command:

python manage.py launch --countdown 60 --project apollo

The countdown option in our example is available in the config argument of the handle method. This object, stores all the options that was passed to the command. There are two ways to access these options:

  1. Access to command line options using config.options.

  2. Access the option using the getoption() method. This method offers more possibilities than just using the config.options method.

In addition to being able to add custom command line options, all management commands can accept some default options such as --verbosity and --settings.

Abstract command

The term abstract means that the command you will define will not be registered and available in the manage.py. It is useful when you want to make a command as an interface for other commands.

Let’s take an example on how to make an abstract command and how it can be used. Let’s take the publish command that we used in this document and make it abstract.

from socon.core.management.base import BaseCommand, Config


class BasePublishCommand(BaseCommand, abstract=True):

    def handle(self, config: Config):
        self.create_article()
        print(f'Publishing an article about NASA')

    def create_article(self):
        raise NotImplementedError(
            "Subclass of must implement the create_article() method"
        )


class PublishCommand(BasePublishCommand):
    name = 'publish'

    def create_article(self):
        print("Are we alone in the universe?")

This example shows a BasePublishCommand that is declared as abstract. This means that it won’t be seen in the manage.py. Only the subclassed command will be seen.

Complementary information

Multiple commands

Multiple commands can be declared in one module. Socon will search for any subclass of BaseCommand and ProjectCommand that are not abstract. This will give you the choice to organize your project as you wish.

Command name

As you might have seen, we always specified the name of a command using the name attribute. This is not mandatory, by default Socon will take the name of your command class in lowercase. If the name of the class contains the word Command at the end of it like PublishCommand. The name will be stripped out and the final name will be publish.

Keep extra args

Sometimes it is useful to pass extra arguments to another script. Socon will allow you to do that by setting the keep_extras_args to True. This way you can access all the extra arguments through extras_args.

Subcommand

Subcommand is a specific type of command. It enables you to define multiple commands under one specific command. To create a subcommand command you need to:

  • Create a class that inherit from Subcommand

  • Create a manager that inherit from SubcommandManager. It’s that manager that will register and hold all subcommands.

  • Create all your subcommands

For our examples we will create a command that holds two subcommands. The architecture will be the following:

myframework/
    myframework/
        management/
            commands/
                subcommands/
                    __init__.py
                    sub1.py
                    sub2.py
                __init__.py
                subcommand.py
            __init__.py
            managers.py
            settings.py
    projects/
    manage.py

As a Subcommand inherit from BaseCommand we register the command in the commands folder with the following content:

myframework/management/commands/subcommand.py
from socon.core.management.subcommand import Subcommand


class BaseSubcommand(Subcommand):
    # Name of the main command
    name = "base-subcommand"

    # Name of the manager that hold all your subcommands
    subcommand_manager = "sub-manager"

Note

As specified in the above section, the name of the file that hold the command does not matter. We used subcomand.py just for the example.

As you can see, we have specified the subcommand_manager class attribute. It’s that attribute that makes the link between the main command class and the subcommands. Now we need to declare the sub-manager manager. For that in the managers.py file we write the following content:

myframework/management/managers.py
from socon.core.management.subcommand import SubcommandManager

class MySubManager(SubcommandManager):
    name = "sub-manager"

Note

By default the SubcommandManager manager will look into the following folder for all subcommands in the common space and in every projects: management/commands/subcommands. If you want to redefine that, you can override the lookup_module class attribute or re-define the get_modules() method.

In our case, the manager will look inside the commands/subcommands folder so we will define our subcommands in there.

myframework/management/commands/subcommands/sub1.py
from socon.core.management.base import BaseCommand, Config

class SubCommandOne(BaseCommand):
    # Name of the subcommand
    name = "sub1"

    # We specify the same manager as the subcommand_manager of the main command
    manager = "sub-manager"

    def handle(self, config: Config) -> None:
        print("Subcommand 1 of the base-subcommand")
myframework/management/commands/subcommands/sub2.py
from socon.core.management.base import ProjectCommand, Config
from socon.core.registry.config import ProjectConfig

class SubCommandOne(ProjectCommand):
    # Name of the subcommand
    name = "sub2"

    # We specify the same manager as the subcommand_manager of the main command
    manager = "sub-manager"

    def handle(self, config: Config, project_conig: ProjectConfig) -> None:
        print("Subcommand 2 of the base-subcommand")

Each subcommand can be of any type (ProjectCommand or BaseCommand) and can be re-define by each project.

Subcommand usage

A command that holds a list of subcommands will have the following usage:

usage: manage.py base-subcommand SUBCOMMAND ...

... helper of the command ...

List of available subcommands:

[myframework]

    sub1
    sub2

To use a subcommand:

$ python manage.py base-subcommand sub1

Warning

Deep subcommand (Subcommand of subcommands) is not yet supported.