From 8b889e9ecd148511c9853505ec825237e118253c Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sun, 15 Jun 2014 20:49:46 +0200 Subject: [PATCH] Port CLI to Click. After a while i finally gave in and ported the whole command line interface of vdirsyncer to Click. While it might seem like a huge dependency, i hope we can eliminate some helper functions in the process, and maybe are more motivated to write a more beautiful and intuitive interface due to all the niceties Click provides. --- setup.py | 2 +- vdirsyncer/cli.py | 56 +++++++++++++----------------------- vdirsyncer/utils/__init__.py | 21 ++++++-------- vdirsyncer/utils/compat.py | 2 -- 4 files changed, 29 insertions(+), 52 deletions(-) diff --git a/setup.py b/setup.py index 63f4161..50f0f31 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ setup( 'console_scripts': ['vdirsyncer = vdirsyncer.cli:main'] }, install_requires=[ - 'argvard>=0.3.0', + 'click', 'requests', 'lxml', 'icalendar>=3.6', diff --git a/vdirsyncer/cli.py b/vdirsyncer/cli.py index 578be7f..0d202b4 100644 --- a/vdirsyncer/cli.py +++ b/vdirsyncer/cli.py @@ -11,7 +11,7 @@ import json import os import sys -import argvard +import click from . import log from .storage import storage_names @@ -206,41 +206,29 @@ def parse_pairs_args(pairs_args, all_pairs): def _main(env, file_cfg): general, all_pairs, all_storages = file_cfg - app = argvard.Argvard() - @app.option('--verbosity verbosity') - def verbose_option(context, verbosity): + @click.group() + @click.option('--verbosity', '-v', default='INFO', + help='Either CRITICAL, ERROR, WARNING, INFO or DEBUG') + def app(verbosity): ''' - Basically Python logging levels. - - CRITICAL: Config errors, at most. - - ERROR: Normal errors, at most. - - WARNING: Problems of which vdirsyncer thinks that it can handle them - itself, but which might crash other clients. - - INFO: Normal output. - - DEBUG: Show e.g. HTTP traffic. Not supposed to be readable by the - normal user. - + vdirsyncer -- synchronize calendars and contacts ''' verbosity = verbosity.upper() x = getattr(log.logging, verbosity, None) if x is None: - raise ValueError(u'Invalid verbosity value: {}'.format(verbosity)) - log.set_level(x) + cli_logger.critical(u'Invalid verbosity value: {}' + .format(verbosity)) + sys.exit(1) + else: + log.set_level(x) - sync_command = argvard.Command() - - @sync_command.option('--force-delete status_name') - def force_delete(context, status_name): - '''Pretty please delete all my data.''' - context.setdefault('force_delete', set()).add(status_name) - - @sync_command.main('[pairs...]') - def sync_main(context, pairs=None): + @app.command() + @click.argument('pairs', nargs=-1) + @click.option('--force-delete', multiple=True, + help=('Disable data-loss protection for the given pairs. ' + 'Can be passed multiple times')) + def sync(pairs, force_delete): ''' Synchronize the given pairs. If no pairs are given, all will be synchronized. @@ -253,7 +241,7 @@ def _main(env, file_cfg): ''' actions = [] handled_collections = set() - force_delete = context.get('force_delete', set()) + force_delete = set(force_delete) for pair_name, _collection in parse_pairs_args(pairs, all_pairs): for collection in expand_collection(pair_name, _collection, all_pairs, all_storages): @@ -293,16 +281,12 @@ def _main(env, file_cfg): from multiprocessing import Pool p = Pool(processes=general.get('processes', 0) or len(actions)) if not all(p.map_async(_sync_collection, actions).get(10**9)): - raise CliError() - - app.register_command('sync', sync_command) + sys.exit(1) try: app() except CliError as e: - msg = str(e) - if msg: - cli_logger.critical(msg) + cli_logger.critical(str(e)) sys.exit(1) diff --git a/vdirsyncer/utils/__init__.py b/vdirsyncer/utils/__init__.py index c9ff753..8ab360e 100644 --- a/vdirsyncer/utils/__init__.py +++ b/vdirsyncer/utils/__init__.py @@ -10,9 +10,10 @@ import os import requests +import click from .. import exceptions, log -from .compat import get_raw_input, iteritems, urlparse +from .compat import iteritems, urlparse logger = log.get(__name__) @@ -155,8 +156,6 @@ def get_password(username, resource): """ - import getpass - for func in (_password_from_netrc, _password_from_keyring): password = func(username, resource) if password is not None: @@ -164,18 +163,14 @@ def get_password(username, resource): .format(username, func.__doc__)) return password - prompt = ('Server password for {} at the resource {}: ' + prompt = ('Server password for {} at the resource {}' .format(username, resource)) - password = getpass.getpass(prompt=prompt) + password = click.prompt(prompt=prompt, hide_input=True) - if keyring is not None: - answer = None - while answer not in ['', 'y', 'n']: - prompt = 'Save this password in the keyring? [y/N] ' - answer = get_raw_input(prompt).lower() - if answer == 'y': - keyring.set_password(password_key_prefix + resource, - username, password) + if keyring is not None and \ + click.confirm('Save this password in the keyring?', default=False): + keyring.set_password(password_key_prefix + resource, + username, password) return password diff --git a/vdirsyncer/utils/compat.py b/vdirsyncer/utils/compat.py index cdc03f6..94dc822 100644 --- a/vdirsyncer/utils/compat.py +++ b/vdirsyncer/utils/compat.py @@ -20,7 +20,6 @@ if PY2: text_type = unicode # flake8: noqa iteritems = lambda x: x.iteritems() itervalues = lambda x: x.itervalues() - get_raw_input = raw_input else: import urllib.parse as urlparse urlquote_plus = urlparse.quote_plus @@ -28,4 +27,3 @@ else: text_type = str iteritems = lambda x: x.items() itervalues = lambda x: x.values() - get_raw_input = input