Python Typer CLI tutorial by @Real Python
-
Building a Real-world application by building a To-Do application on the command-line interface using the Typer library.
-
In this project, we will be able to learn how to:
- Build a functional to-do application with a Typer CLI in Python
- Use Typer to add commands, arguments, and options to your to-do app
- Test your Python to-do application with Typer's CliRunner and Pytest
- We will also be able to practice our skills related to processing JSON files by using Python's JSON module and managing configuration files with Python's configparser module.
Project Overview
-
Helpful commands and options that the user's of this CLI will be able to perform:
-
-v or --version shows the current version and exits the application
-
--help shows the global help message for the entire application
-
Here are other important options for the to-do application that the user can be able to accomplish:
Command | Description |
---|---|
init | Initializes the application's to-do database |
add DESCRIPTION | Adds a new to-do to the database with a description |
list | Lists all the to-dos in the database |
complete TODO_ID | Lists all the to-dos in the database |
complete TODO_ID | Completes a to-do by setting it as done using its ID |
remove TODO_ID | Removes a to-do from the database using its ID |
clear | Removes all the to-dos by clearing the database |
- Here are the following tasks that we need to accomplish in order to build a Minimum Viable Product (MVP) for our users:
- Build a command-line interface capable of taking and processing commands, options and arguments.
- Select an appropriate data type to represent your to-dos
- Implement a way to persistenly store your to-do list
- Define a way to connect that user interface with the to-do data
-
In the following tasks that we have stated above, we have taken into consideration a very well-known architectural pattern known as the Model-View-Controller design.
-
In this pattern, the model takes care of the data, the view deals with the user interface, and the controller connects both ends to make the application work.
-
Using this pattern in this CLI To-Do application and projects provides separation of concerns (SOC), in which it is making different parts of your code deal with specific concepts independently.
The software stack to be used in the CLI To-Do Application
- Typer library to build the to-do application's CLI
- Named tuples and dictionaries to handle the to-do data
- Python's json module to manage persistent data storage
- Using configparser module from the Python standard library to handle the application's initial settings in a configuration file.
- Using pytest testing framework to test our CLI application
Setting-up the Working Environment
(cli_to_do_dev) C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli>
Defining our Project Layout
Command | Description |
---|---|
init.py | Enables our to_do_cli/ to be a Python package |
main.py | Provides an entry-point script to run the app from the package using the python -m to_do_cli command |
cli.py | Provides the Typer command-line interface for the application |
config.py | Contains code to handle the application's configuration file |
database.py | Contains code to handle the application's to-do database |
todo_connect.py | Provides code to connect the CLI with the to-do database |
- We will also be creating a tests/ directory containing a init.py to turn also our test directory into a package and test_crtodo.py file to hold unit testing for the CLI To-Do application.
Application Layout Structure
to_do_cli/ │ ├── crtodo/ │ ├── init.py │ ├── main.py │ ├── cli.py │ ├── config.py │ ├── database.py │ └── todo_connect.py │ ├── tests/ │ ├── init.py │ └── test_crtodo.py │ ├── README.md └── requirements.txt
Initial Look for our CLI To-Do application
(cli_to_do_dev) C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli>py -m crtodo -v
crtodo v0.1.0
(cli_to_do_dev) C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli>py -m crtodo --help
Usage: crtodo [OPTIONS] COMMAND [ARGS]...
Options:
-v, --version Show the application's version and exit.
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or
customize the installation.
--help Show this message and exit.
Setting up Initial CLI Tests with pytest
-
Typer class that suits well with testing CLI application -> CliRunner
-
NOTE: Always type in the flag 'py -m crtodo' in order to run your module
(cli_to_do_dev) C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli>py -m pytest --verbose tests/
================================================= test session starts =================================================
platform win32 -- Python 3.9.0, pytest-6.2.4, py-1.11.0, pluggy-0.13.1 -- C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli
collected 1 item
tests/test_crtodo.py::test_version PASSED [100%]
================================================== 1 passed in 0.07s ==================================================
Preparing the To-Do Database for Use
- Setting up the directory path for our configuration file to store our database data objects.
- In order for our database to be ready for use, we need to setup two things:
- We need a way to retrieve the database file path from the application's configuration file.
- We need to initialize the database to hold JSON content.
Implementing the init CLI Command
(cli_to_do_dev) C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli>py -m crtodo init
to-do database location? [C:\Users\creyes24\.creyes24_todo.json]: C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli\._todo.json
The to-do database is C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli\._todo.json
Setting up the To-Do App Back End
- Defining a Single To-Do
Our main components that is stored in our Single To-Do:
- Description: How do you describe this to-do?
- Priority: What priority does this to-do have over the rest of your to-dos?
- Done: Is this to-do done?
Our sample To-Do data structure:
todo = {
"Description": "Finish cooking bolognese",
"Priority": 4,
"Done": True,
}
The "Description" key stores a string describing the current to-do. The "Priority" key can take three possible values: 1 for high, 2 for medium, and 3 for low priority. The "Done" key holds True when you've completed the to-do and False otherwise.
- Communicating with the CLI
- todo: The dictionary holding the information for the current to-do
- error: The return or error code confirming if the current operation was successful or not.
-
For our To-Do model controller, we subclassed NamedTuple from the typing module as this allows us to create named tuples with type hints for their named fields.
-
Communicate with the Database
- todo_list: The to-do list you'll write to and read from the database
- error: An integer number representing a return code related to the current database operation.
- Write the Controller Class, Todoer
In order to connect our DatabaseHandler logic with our application's CLI, you'll write a class called Todoer. This class will be able to replicate the behaviour similarly to a controller in the Model-View-Controller pattern
Our Todoer class uses the composition OOP design pattern, in which it has a DatabaseHandler component to facilitate direct communication with the to-do database.
Adding and Listing To-Dos Functionalities
- Define Unit Tests for Todoer.add()
On our add functionality of our To-Do CLI application, think of what '.add()' method really does:
- Get a to-do description and priority
- Create a dictionary to hold the to-do information
- Read the to-do list from the database
- Append the new to-do to the current to-do list
- Write the updated to-do list back to the database.
- Return the newly added to-do along with a return code back to the caller.
-
A common practice in TDD is to start with the main functionality of a given method or function. We will start by creating test cases to check if .add() properly adds new to-dos to the database.
-
To test .add(), we must be able to create a Todoer instance with a proper JSON file as the target database. In order to provide the file, we will use a pytest fixture
Sample test cases for the .add() functionality
(cli_to_do_dev) C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli>py -m pytest --verbose tests/
================================================= test session starts =================================================
platform win32 -- Python 3.9.0, pytest-6.2.4, py-1.11.0, pluggy-0.13.1 -- C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\creyes24\Real-World-Python\CLI_projects\to_do_cli
collected 3 items
tests/test_crtodo.py::test_version PASSED [ 33%]
tests/test_crtodo.py::test_add[description0-1-expected0] FAILED [ 66%]
tests/test_crtodo.py::test_add[description1-2-expected1] FAILED [100%]
====================================================== FAILURES =======================================================
_________________________________________ test_add[description0-1-expected0] __________________________________________
mock_json_file = WindowsPath('C:/Users/creyes24/AppData/Local/Temp/pytest-of-creyes24/pytest-0/test_add_description0_1_expect0/todo.json')
description = ['get', 'some', 'coffee', 'kick', 'smoothie'], priority = 1
expected = ({'Description': 'Get some Coffee Kick Smoothie.', 'Done': False, 'Priority': 1}, 0)
@pytest.mark.parametrize(
"description, priority, expected",
[
pytest.param(
test_data1["description"],
test_data1["priority"],
(test_data1["todo"], SUCCESS),
),
pytest.param(
test_data2["description"],
test_data2["priority"],
(test_data2["todo"], SUCCESS),
),
],
)
def test_add(mock_json_file, description, priority, expected):
todoer = crtodo.Todoer(mock_json_file)
> assert todoer.add(description, priority) == expected
E AttributeError: 'Todoer' object has no attribute 'add'
tests\test_crtodo.py:72: AttributeError
_________________________________________ test_add[description1-2-expected1] __________________________________________
mock_json_file = WindowsPath('C:/Users/creyes24/AppData/Local/Temp/pytest-of-creyes24/pytest-0/test_add_description1_2_expect0/todo.json')
description = ['Wash the car'], priority = 2
expected = ({'Description': 'Wash the car.', 'Done': False, 'Priority': 2}, 0)
@pytest.mark.parametrize(
"description, priority, expected",
[
pytest.param(
test_data1["description"],
test_data1["priority"],
(test_data1["todo"], SUCCESS),
),
pytest.param(
test_data2["description"],
test_data2["priority"],
(test_data2["todo"], SUCCESS),
),
],
)
def test_add(mock_json_file, description, priority, expected):
todoer = crtodo.Todoer(mock_json_file)
> assert todoer.add(description, priority) == expected
E AttributeError: 'Todoer' object has no attribute 'add'
tests\test_crtodo.py:72: AttributeError
=============================================== short test summary info ===============================================
FAILED tests/test_crtodo.py::test_add[description0-1-expected0] - AttributeError: 'Todoer' object has no attribute 'add'
FAILED tests/test_crtodo.py::test_add[description1-2-expected1] - AttributeError: 'Todoer' object has no attribute 'add'
============================================= 2 failed, 1 passed in 0.43s =============================================
Implementing the add CLI Command
- Every time the to-do application runs, it needs to be accessed from the Todoer class and connect the CLI with the database. To accomplish this requirement, we will create a get_todoer() function.
Implementing the list Command
Implementing the To-Do Completion Functionality
Implementing the Remove To-Dos functionality
Implementing the remove CLI command
Implementing the clear CLI command
Possible feature additions to the minimal To-Do CLI Application
- Add support for dates and deadlines
- Write more unit tests
- Packing the application and publishing it to PyPI