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:
BaseCommandthat we call general command. These commands are considered global as they don’t require to load aProjectConfig. They are essentially used to execute general tasks that are not project related.ProjectCommandinherit fromBaseCommand. These commands require a project configuration to work. For this command to work you will need to specify the--projectoption.Subcommandinherit fromBaseCommand. By subclassing your command from this class, you can create a command that has it’s own list ofProjectCommandandBaseCommand.
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:
- Search in Socon config for built-in commands.
If the command is found save it.
- Search in the plugins.
If the command is found, override the previous command.
- Search in the common space config for commands.
If the command is found, override the previous command.
- 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:
Access to command line options using
config.options.Access the option using the
getoption()method. This method offers more possibilities than just using theconfig.optionsmethod.
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
SubcommandCreate 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:
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:
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.
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")
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.