Tutorial: Todo List¶
Let’s develop a todo list using easycli.
it’s highly recommended to use virtual environment before we continue. I use virtualenvwrapper.
Create a virtual environment to isolate your hello world application from the rest of the system python packages.
mkvirtualenv todo
workon todo
First, we’re going to create a classic python module including a
setup.py to be able to install it.
Create a directory named todo containing a python module named todo.py
:
import easycli
__version__ = '0.1.0'
class Todo(easycli.Root):
__help__ = 'Simple todo list'
__arguments__ = [
easycli.Argument(
'-v', '--version',
action='store_true',
help='Show version'
),
]
def __call__(self, args):
if args.version:
print(__version__)
return
return super().__call__(args)
And a setup.py:
import re
from os.path import join, dirname
from setuptools import setup
with open(join(dirname(__file__), 'todo.py')) as f:
version = re.match('.*__version__ = \'(.*?)\'', f.read(), re.S).group(1)
dependencies = [
'easycli',
]
setup(
name='todo',
version=version,
py_modules=['todo'],
install_requires=dependencies,
include_package_data=True,
license='MIT',
entry_points={
'console_scripts': [
'todo = todo:Todo.quickstart',
]
}
)
Note
setuptools offers an argument entry_points
which is helpful here.
Then install your project in editable mode:
cd /path/to/todo
pip3 install -e .
Test your command line interface with:
todo --help
usage: todo [-h] [-v]
Simple todo list
optional arguments:
-h, --help show this help message and exit
-v, --version Show version
Test the -v/--version flag:
todo -v
todo --version
Append Command¶
functools helps keep our code DRY. Here is how to create a command
to append a line list,item to a csv file.
from os.path import join, dirname
import functools
opendbfile = functools.partial(
open,
join(dirname(__file__), 'data.csv')
)
class Append(easycli.SubCommand):
__command__ = 'append'
__aliases__ = ['add', 'a']
__arguments__ = [
easycli.Argument(
'list',
default='',
help='List name',
),
easycli.Argument(
'item',
help='Item name',
)
]
def __call__(self, args):
with opendbfile('a+') as f:
f.write(f'{args.list},{args.item}\n')
Add the Append command class to Todo.__arguments__ collection without
instantiating it:
class Todo(easycli.Root):
...
__arguments__ = [
...,
Append
]
Now, see the newly added command (append and it’s aliases: add,a)
in -h/--help output:
todo --help
usage: todo [-h] [-v] {append,add,a} ...
Simple todo list
optional arguments:
-h, --help show this help message and exit
-v, --version Show version
Sub commands:
{append,add,a}
append (add, a)
Add an item using:
todo append foo bar
# Or
todo add foo bar
# Or
todo a foo bar
Let’s modify our code and use functools to create a reusable
Argument factory.
ListArgument = functools.partial(
easycli.Argument,
'list',
default='',
help='List name',
)
ItemArgument = functools.partial(
easycli.Argument,
'item',
help='Item name',
)
class Append(easycli.SubCommand):
...
__arguments__ = [
ListArgument(),
ItemArgument(),
]
...
Show Command¶
We need a command to show lists or items inside a list.
def getall(*a, **k):
with opendbfile(*a, **k) as f:
for l in f:
yield l.strip().split(',', 1)
class Show(easycli.SubCommand):
__command__ = 'show'
__aliases__ = ['s', 'l']
__arguments__ = [
ListArgument(nargs='?')
]
def __call__(self, args):
if args.list:
for l, i in getall():
if l == args.list:
print(i)
else:
for l, i in getall():
print(f'{l}\t{i}')
Add the Show command class to Todo.__arguments__ collection without
instantiating it:
class Todo(easycli.Root):
...
__arguments__ = [
...,
Append,
Show
]
Test it:
todo show
todo show foo
# Or
todo l
todo l foo
Delete Command¶
class Delete(easycli.SubCommand):
__command__ = 'delete'
__aliases__ = ['d']
__arguments__ = [
ListArgument(),
ItemArgument(),
]
def __call__(self, args):
list_ = args.list
item = args.item
data = [(l, i) for l, i in getall() if l != list_ or i != item]
with opendbfile('w') as f:
for l, i in data:
f.write(f'{l},{i}\n')
...
class Todo(easycli.Root):
...
__arguments__ = [
...,
Append,
Show,
Delete
]
Now, you can add, show and delete your todo items.
todo delete foo bar
todo d foo bar
Completion¶
I love bash auto completion.
So, the first step to do that is to set the __completion__ class attribute
of the Todo class.
Thanks to Argcomplete.
class Todo(easycli.Root):
...
__completion__ = True
...
Take a look at the help message:
usage: todo [-h] [-v] {append,add,a,show,s,l,delete,d,completion} ...
Simple todo list
optional arguments:
-h, --help show this help message and exit
-v, --version Show version
Sub commands:
{append,add,a,show,s,l,delete,d,completion}
append (add, a)
show (s, l)
delete (d)
completion Bash auto completion using argcomplete python package.
As you see the completion sub command has been added.
todo completion --help
usage: todo completion [-h] {install,uninstall} ...
optional arguments:
-h, --help show this help message and exit
Sub commands:
{install,uninstall}
install Enables autocompletion.
uninstall Disables autocompletion.
This is how to enable the bash auto completion
todo completion install
After this, to reload and apply changes you need to deactivate and
activate your virtual env again.
# virtualenvwrapper
deactivate && workon todo
Type todo and hit the TAB key twice to see the result.
$ todo
a append d -h l show --version
add completion delete --help s -v
Dynamic Autocompletion¶
How about implementing autocompletion for list and or items.
We have to write two functions to get the available lists and items.
def listcompleter(prefix, action, parser, parsed_args):
return set(l for l, _ in getall())
def itemcompleter(prefix, action, parser, parsed_args):
list_ = parsed_args.list
return list(i for l, i in getall() if l == list_)
Then modify our arguments to use those functions as their completers:
ListArgument = functools.partial(
easycli.Argument,
'list',
default='',
help='List name',
completer=listcompleter
)
ItemArgument = functools.partial(
easycli.Argument,
'item',
help='Item name',
completer=itemcompleter
)
This is the complete version of the todo.py:
from os.path import join, dirname
import functools
import easycli
__version__ = '0.1.0'
opendbfile = functools.partial(
open,
join(dirname(__file__), 'data.csv')
)
def getall(*a, **k):
with opendbfile(*a, **k) as f:
for l in f:
yield l.strip().split(',', 1)
def listcompleter(prefix, action, parser, parsed_args):
return set(l for l, _ in getall())
def itemcompleter(prefix, action, parser, parsed_args):
list_ = parsed_args.list
return list(i for l, i in getall() if l == list_)
ListArgument = functools.partial(
easycli.Argument,
'list',
default='',
help='List name',
completer=listcompleter
)
ItemArgument = functools.partial(
easycli.Argument,
'item',
help='Item name',
completer=itemcompleter
)
class Delete(easycli.SubCommand):
__command__ = 'delete'
__aliases__ = ['d']
__arguments__ = [
ListArgument(),
ItemArgument(),
]
def __call__(self, args):
list_ = args.list
item = args.item
data = [(l, i) for l, i in getall() if l != list_ or i != item]
with opendbfile('w') as f:
for l, i in data:
f.write(f'{l},{i}\n')
class Append(easycli.SubCommand):
__command__ = 'append'
__aliases__ = ['add', 'a']
__arguments__ = [
ListArgument(),
ItemArgument(),
]
def __call__(self, args):
with opendbfile('a+') as f:
f.write(f'{args.list},{args.item}\n')
class Show(easycli.SubCommand):
__command__ = 'show'
__aliases__ = ['s', 'l']
__arguments__ = [
ListArgument(nargs='?')
]
def __call__(self, args):
if args.list:
for l, i in getall():
if l == args.list:
print(i)
else:
for l, i in getall():
print(f'{l}\t{i}')
class Todo(easycli.Root):
__help__ = 'Simple todo list'
__completion__ = True
__arguments__ = [
easycli.Argument(
'-v', '--version',
action='store_true',
help='Show version'
),
Append,
Show,
Delete
]
def __call__(self, args):
if args.version:
print(__version__)
return
return super().__call__(args)
Enjoy your very own todo list.
The complete code is available as a python project on github: easycli-todolist-demo.