Package as a feature
Now it is time to add our package as features:
$ mkdir storage_X/cli/dev/service
$ touch storage_X/cli/dev/service/__init__.py
$ mkdir storage_Y/cli/dev/environment
$ touch storage_Y/cli/dev/environment/__init__.py
That's it you can now run your CLI:
$ ./awesome -h
usage: awesome [-h] {service,environment} ...
positional arguments:
{service, environment}
service [ERROR] Missing the module docstring
environment [ERROR] Missing the module docstring
optional arguments:
-h, --help show this help message and exit
Our packages have no docstrings in them, due to this fact we got an ERROR
indicating that we are missing the docstrings.
Let's quickly fix this. We are going to add docstrings to the __init__.py
files.
Open the storage_X/cli/dev/service/__init__.py
and add the following:
Open the storage_Y/cli/dev/environment/__init__.py
and add the following:
Now if you rerun the CLI you can see that there are no ERROR
s:
$ ./awesome -h
usage: awesome [-h] {service,environment} ...
positional arguments:
{service, environment}
service The service feature to handle our services
environment The environment feature to handle our environments
optional arguments:
-h, --help show this help message and exit
Feature commands
What kind of operations do we want for our service feature?
Let's imagine that we can create, update, and shut down the services.
That means we need new.py
, update.py
, and shutdown.py
files in the service package:
$ touch storage_X/cli/dev/service/new.py
$ touch storage_X/cli/dev/service/update.py
$ touch storage_X/cli/dev/service/shutdown.py
We consider commands in the package as features if they have an identically named function in them.
In other words, there should be new()
function in new.py
, update()
in update.py
etc.
So, let's define our functions(feature commands):
def new(name: str, path: str):
"""
init the new project in the given path
Args:
name (str): name of the project
path (str): path where to create service
Return: None
"""
print(f"Initializing the {name} in {path}")
def update(name: str, version: float, upgrade: bool, *args: str, **kwargs: int) -> None:
"""
updates the service...
Args:
name (str): name of the service
version (float): new version
upgrade (bool): if to upgrade everything
*args (str): variable length arguments
**kwargs (int): keyword arguments
Return: None
"""
print(f"Updating...{name} to {version} with {upgrade=} using {args} and {kwargs}")
def shutdown(environment: str, service: str) -> None:
"""
shutdown the service
Args:
environment (str): environment name (e.g. Cloud9 IDE stack)
service (str): name of the service
Return: None
"""
print(f"This is a shutdown of {service} from {environment}!")
Now let's get information about service feature:
$ ./awesome service -h
usage: awesome service [-h] {new,shutdown,update} ...
positional arguments:
{new,shutdown,update}
new init the new project in the given path
shutdown shutdown the service
update updates the service...
optional arguments:
-h, --help show this help message and exit
How about each command?
$ ./awesome service update -h
usage: awesome service update [-h] name version upgrade [args ...] [kwargs <name>=<value> ...]
positional arguments:
name name of the service
version new version
upgrade if to upgrade everything
args variable length arguments
kwargs <name>=<value>
keyword arguments
optional arguments:
-h, --help show this help message and exit
Now let's call the update command:
$ ./awesome service update myservice 2.0 True lib1 lib2 version1=1.2 version2=1.3
Updating... myservice to 2.0 with upgrade=True using ('lib1', 'lib2') and {'version1': 1.2, 'version2': 1.3}
As you have already noticed we have converted the CLI commands to the function arguments with proper type conversion.
Versioning your features and commands
Now imagine the case, when for some reason you have a bunch of features with different versions, and also your commands have different versioning.
You can easily handle it, by adding __version__
in the feature and commands.
Open the storage_X/cli/dev/service/__init__.py
and add:
Now you can get the version of the feature:
Same for update
command:
__version__ = "2.0"
def update(name: str, version: float, upgrade: bool, *args: str, **kwargs: float) -> None:
...
Limiting the feature commands
You may have a situation when you have other helper modules inside the feature package, and you do not want to expose them as a feature command.
In that case, you can leverage the __all__
mechanism. Originally in Python __all__
only limits the imports such as: from something import *
.
But here we use it just for eliminating the redundant operations when we register the feature commands.
So let's eliminate the shutdown
command from our service
feature without removing it.
Update the __init__.py
file of the service feature:
"""The service feature to handle our services"""
from . import *
__version__ = "1.0"
__all__ = ["new", "update"]
And now try to get the help, as you have already noticed shutdown
command is not available:
$ ./awesome service -h
usage: awesome service [-h] [-v] {new,update} ...
positional arguments:
{new,update}
new init the new project in given path
update Updates the service...
optional arguments:
-h, --help show this help message and exit
-v, --version show program's version number and exit
If you try to bypass this guard(because you know that there is a shutdown.py file indeed):
$ ./awesome service shutdown -h
usage: awesome service [-h] [-v] {new,update} ...
awesome service: error: invalid choice: 'shutdown' (choose from 'new', 'update')
The next is to explore modules as features.