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.