Recently, I wanted to start my open source journey by contributing to an open source project called wily.
The issue I was working on was to first add a version flag to print the current version of the tool and second to add the version to the help page.
As the project uses click to create a user friendly command-line interface, the help page is auto-generated based on the docstrings.
That’s great, but refuses to inject custom dynamic texts.
To recall the example of adding the version to the help page, you need to update your version twice: Inside of the
__init__.py and inside of the docstring of the main cli command.
But you don’t want to change the version of the tool at two places.
So, how can you dynamically inject it into the docstring?
A quick research about functions docstrings reveals, that you can access and modify them using
Problem: You can’t do it inside of the function itself as the docstring is added after function creation.
Solution: Modify the docstring afterwards.
And to anticipate decorators are great for that!
So, let’s demonstrate that by an example project. Consider the following project structure:
└── tool ├── __init__.py ├── __main__.py └── custom_decorators.py
tool is our tools directory, the
__main__.py will include the logic of the command-line tool and custom_decorators.py contains our decorator, which we will use later to change the help page.
Let’s start by having a look at the
""" Tool. Example tool. """ __version__ = "1.2.3"
As you can see, it only contains the
__version__ of the tool.
Next, we will have a look at the
import click from tool import __version__ @click.group() @click.version_option( __version__, "-V", "--version", message="%(prog)s, version %(version)s" ) @click.pass_context def cli(ctx): """ Welcome to the Help Page of tool. $ tool ... """ pass @cli.command() @click.option( "-t", "--text", default="Hello World", help="Text to print" ) @click.pass_context def do(ctx, text): """Print text.""" print(text) if __name__ == "__main__": cli()
It’s just a very basic command-line tool created with click.
If you are not very familiar with the code, check out the documentation.
You are able to print the version using
tool --version and print some text using the
$ python tool/__main__.py do "Some Text"
As already mentioned, you are able to display the help page using
--help natively, which is generated based on the docstrings.
Let’s modify the help page a little bit!
Therefore, we will add the tools version to the help pages heading.
First of all, we need a custom decorator modifying the
__doc__ attribute of the cli function, which is our main method or group in this case.
Let’s call the decorator
add_version and put the corresponding code into tool/custom_decorators.py.
The code is pretty straightforward:
""" A module including custom decorators. """ from . import __version__ def add_version(f): """ Add the version of the tool to the help heading. :param f: function to decorate :return: decorated function """ doc = f.__doc__ f.__doc__ = "Version: " + __version__ + "\n\n" + doc return f
Now, we only need to add the decorator to our cli function before all other click decorators are applied:
import click from tool import __version__ from tool.custom_decorators import add_version @click.group() @click.version_option( __version__, "-V", "--version", message="%(prog)s, version %(version)s" ) @click.pass_context @add_version def cli(ctx): """ Welcome to the Help Page of tool. $ tool ... """ pass @cli.command() @click.option( "-t", "--text", default="Hello World", help="Text to print" ) @click.pass_context def do(ctx, text): """Print text.""" print(text) if __name__ == "__main__": cli()
That’s it! We changed the auto-generated help page and injected the tools version using a custom decorator. The whole project can be found on GitHub. Feel free to get it running on your own machine, to extend it further and to share it!
This post was originally published to Medium.