From cfe252d458c5b302000743a1ee599ffc21a01012 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sat, 20 Dec 2014 01:59:59 +0100 Subject: [PATCH] Make .cli a subpackage --- tests/test_cli.py | 11 ++- vdirsyncer/cli/__init__.py | 147 ++++++++++++++++++++++++++++ vdirsyncer/{cli.py => cli/utils.py} | 146 ++------------------------- 3 files changed, 161 insertions(+), 143 deletions(-) create mode 100644 vdirsyncer/cli/__init__.py rename vdirsyncer/{cli.py => cli/utils.py} (77%) diff --git a/tests/test_cli.py b/tests/test_cli.py index 17c0af5..0166f15 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -66,7 +66,7 @@ def test_storage_instance_from_config(monkeypatch): import vdirsyncer.storage monkeypatch.setitem(vdirsyncer.storage.storage_names, 'lol', lol) config = {'type': 'lol', 'foo': 'bar', 'baz': 1} - assert cli.storage_instance_from_config(config) == 'OK' + assert cli.utils.storage_instance_from_config(config) == 'OK' def test_parse_pairs_args(): @@ -246,10 +246,13 @@ def test_deprecated_item_status(tmpdir): 'ident_two': ['href_a', 'etag_a', 'href_b', 'etag_b'] } - assert cli.load_status(str(tmpdir), 'mypair', data_type='items') == data + assert cli.utils.load_status( + str(tmpdir), 'mypair', data_type='items') == data - cli.save_status(str(tmpdir), 'mypair', data_type='items', data=data) - assert cli.load_status(str(tmpdir), 'mypair', data_type='items') == data + cli.utils.save_status( + str(tmpdir), 'mypair', data_type='items', data=data) + assert cli.utils.load_status( + str(tmpdir), 'mypair', data_type='items') == data def test_collections_cache_invalidation(tmpdir): diff --git a/vdirsyncer/cli/__init__.py b/vdirsyncer/cli/__init__.py new file mode 100644 index 0000000..5bbf1e1 --- /dev/null +++ b/vdirsyncer/cli/__init__.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +''' + vdirsyncer.cli + ~~~~~~~~~~~~~~ + + :copyright: (c) 2014 Markus Unterwaditzer & contributors + :license: MIT, see LICENSE for more details. +''' + +import functools +import os +import sys + +from .utils import CliError, WorkerQueue, cli_logger, collections_for_pair, \ + handle_cli_error, load_config, parse_pairs_args, sync_pair +from .. import __version__, log +from ..doubleclick import click +from ..utils import expand_path + + +def catch_errors(f): + @functools.wraps(f) + def inner(*a, **kw): + try: + f(*a, **kw) + except: + if not handle_cli_error(): + sys.exit(1) + + return inner + + +def validate_verbosity(ctx, param, value): + x = getattr(log.logging, value.upper(), None) + if x is None: + raise click.BadParameter('Invalid verbosity value {}. Must be ' + 'CRITICAL, ERROR, WARNING, INFO or DEBUG' + .format(value)) + return x + + +@click.group() +@click.option('--verbosity', '-v', default='INFO', + callback=validate_verbosity, + help='Either CRITICAL, ERROR, WARNING, INFO or DEBUG') +@click.version_option(version=__version__) +@click.pass_context +@catch_errors +def app(ctx, verbosity): + ''' + vdirsyncer -- synchronize calendars and contacts + ''' + log.add_handler(log.stdout_handler) + log.set_level(verbosity) + + if ctx.obj is None: + ctx.obj = {} + + if 'config' not in ctx.obj: + fname = expand_path(os.environ.get('VDIRSYNCER_CONFIG', + '~/.vdirsyncer/config')) + if not os.path.exists(fname): + xdg_config_dir = os.environ.get('XDG_CONFIG_HOME', + expand_path('~/.config/')) + fname = os.path.join(xdg_config_dir, 'vdirsyncer/config') + try: + with open(fname) as f: + ctx.obj['config'] = load_config(f) + except Exception as e: + raise CliError('Error during reading config {}: {}' + .format(fname, e)) + +main = app + +max_workers_option = click.option( + '--max-workers', default=0, type=click.IntRange(min=0, max=None), + help=('Use at most this many connections, 0 means unlimited.') +) + + +@app.command() +@click.argument('pairs', nargs=-1) +@click.option('--force-delete/--no-force-delete', + help=('Disable data-loss protection for the given pairs. ' + 'Can be passed multiple times')) +@max_workers_option +@click.pass_context +@catch_errors +def sync(ctx, pairs, force_delete, max_workers): + ''' + Synchronize the given collections or pairs. If no arguments are given, + all will be synchronized. + + Examples: + `vdirsyncer sync` will sync everything configured. + `vdirsyncer sync bob frank` will sync the pairs "bob" and "frank". + `vdirsyncer sync bob/first_collection` will sync "first_collection" + from the pair "bob". + ''' + general, all_pairs, all_storages = ctx.obj['config'] + + cli_logger.debug('Using {} maximal workers.'.format(max_workers)) + wq = WorkerQueue(max_workers) + wq.handled_jobs = set() + + for pair_name, collections in parse_pairs_args(pairs, all_pairs): + wq.spawn_worker() + wq.put( + functools.partial(sync_pair, pair_name=pair_name, + collections_to_sync=collections, + general=general, all_pairs=all_pairs, + all_storages=all_storages, + force_delete=force_delete)) + + wq.join() + + +@app.command() +@click.argument('pairs', nargs=-1) +@max_workers_option +@click.pass_context +@catch_errors +def discover(ctx, pairs, max_workers): + ''' + Refresh collection cache for the given pairs. + ''' + general, all_pairs, all_storages = ctx.obj['config'] + cli_logger.debug('Using {} maximal workers.'.format(max_workers)) + wq = WorkerQueue(max_workers) + + for pair in (pairs or all_pairs): + try: + name_a, name_b, pair_options = all_pairs[pair] + except KeyError: + raise CliError('Pair not found: {}\n' + 'These are the pairs found: {}' + .format(pair, list(all_pairs))) + + wq.spawn_worker() + wq.put(functools.partial( + (lambda wq, **kwargs: collections_for_pair(**kwargs)), + status_path=general['status_path'], name_a=name_a, + name_b=name_b, pair_name=pair, config_a=all_storages[name_a], + config_b=all_storages[name_b], pair_options=pair_options, + skip_cache=True)) + + wq.join() diff --git a/vdirsyncer/cli.py b/vdirsyncer/cli/utils.py similarity index 77% rename from vdirsyncer/cli.py rename to vdirsyncer/cli/utils.py index 6fd464f..ecb7438 100644 --- a/vdirsyncer/cli.py +++ b/vdirsyncer/cli/utils.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- ''' - vdirsyncer.cli - ~~~~~~~~~~~~~~ + vdirsyncer.cli.utils + ~~~~~~~~~~~~~~~~~~~~ :copyright: (c) 2014 Markus Unterwaditzer & contributors :license: MIT, see LICENSE for more details. @@ -17,11 +17,11 @@ import sys import threading from itertools import chain -from . import DOCS_HOME, PROJECT_HOME, __version__, log -from .doubleclick import click -from .storage import storage_names -from .sync import StorageEmpty, SyncConflict, sync -from .utils import expand_path, get_class_init_args, parse_options, \ +from .. import DOCS_HOME, PROJECT_HOME, log +from ..doubleclick import click +from ..storage import storage_names +from ..sync import StorageEmpty, SyncConflict, sync +from ..utils import expand_path, get_class_init_args, parse_options, \ safe_write @@ -380,138 +380,6 @@ def parse_pairs_args(pairs_args, all_pairs): return rv.items() -# We create the app inside a factory and destroy that factory after first use -# to avoid pollution of the module namespace. - - -def _create_app(): - def catch_errors(f): - @functools.wraps(f) - def inner(*a, **kw): - try: - f(*a, **kw) - except: - if not handle_cli_error(): - sys.exit(1) - - return inner - - def validate_verbosity(ctx, param, value): - x = getattr(log.logging, value.upper(), None) - if x is None: - raise click.BadParameter('Invalid verbosity value {}. Must be ' - 'CRITICAL, ERROR, WARNING, INFO or DEBUG' - .format(value)) - return x - - @click.group() - @click.option('--verbosity', '-v', default='INFO', - callback=validate_verbosity, - help='Either CRITICAL, ERROR, WARNING, INFO or DEBUG') - @click.version_option(version=__version__) - @click.pass_context - @catch_errors - def app(ctx, verbosity): - ''' - vdirsyncer -- synchronize calendars and contacts - ''' - log.add_handler(log.stdout_handler) - log.set_level(verbosity) - - if ctx.obj is None: - ctx.obj = {} - - if 'config' not in ctx.obj: - fname = expand_path(os.environ.get('VDIRSYNCER_CONFIG', - '~/.vdirsyncer/config')) - if not os.path.exists(fname): - xdg_config_dir = os.environ.get('XDG_CONFIG_HOME', - expand_path('~/.config/')) - fname = os.path.join(xdg_config_dir, 'vdirsyncer/config') - try: - with open(fname) as f: - ctx.obj['config'] = load_config(f) - except Exception as e: - raise CliError('Error during reading config {}: {}' - .format(fname, e)) - - max_workers_option = click.option( - '--max-workers', default=0, type=click.IntRange(min=0, max=None), - help=('Use at most this many connections, 0 means unlimited.') - ) - - @app.command() - @click.argument('pairs', nargs=-1) - @click.option('--force-delete/--no-force-delete', - help=('Disable data-loss protection for the given pairs. ' - 'Can be passed multiple times')) - @max_workers_option - @click.pass_context - @catch_errors - def sync(ctx, pairs, force_delete, max_workers): - ''' - Synchronize the given collections or pairs. If no arguments are given, - all will be synchronized. - - Examples: - `vdirsyncer sync` will sync everything configured. - `vdirsyncer sync bob frank` will sync the pairs "bob" and "frank". - `vdirsyncer sync bob/first_collection` will sync "first_collection" - from the pair "bob". - ''' - general, all_pairs, all_storages = ctx.obj['config'] - - cli_logger.debug('Using {} maximal workers.'.format(max_workers)) - wq = WorkerQueue(max_workers) - wq.handled_jobs = set() - - for pair_name, collections in parse_pairs_args(pairs, all_pairs): - wq.spawn_worker() - wq.put( - functools.partial(sync_pair, pair_name=pair_name, - collections_to_sync=collections, - general=general, all_pairs=all_pairs, - all_storages=all_storages, - force_delete=force_delete)) - - wq.join() - - @app.command() - @click.argument('pairs', nargs=-1) - @max_workers_option - @click.pass_context - @catch_errors - def discover(ctx, pairs, max_workers): - ''' - Refresh collection cache for the given pairs. - ''' - general, all_pairs, all_storages = ctx.obj['config'] - cli_logger.debug('Using {} maximal workers.'.format(max_workers)) - wq = WorkerQueue(max_workers) - - for pair in (pairs or all_pairs): - try: - name_a, name_b, pair_options = all_pairs[pair] - except KeyError: - raise CliError('Pair not found: {}\n' - 'These are the pairs found: {}' - .format(pair, list(all_pairs))) - - wq.spawn_worker() - wq.put(functools.partial( - (lambda wq, **kwargs: collections_for_pair(**kwargs)), - status_path=general['status_path'], name_a=name_a, - name_b=name_b, pair_name=pair, config_a=all_storages[name_a], - config_b=all_storages[name_b], pair_options=pair_options, - skip_cache=True)) - - wq.join() - - return app - -app = main = _create_app() -del _create_app - def sync_pair(wq, pair_name, collections_to_sync, general, all_pairs, all_storages, force_delete):