diff --git a/gramps/cli/arghandler.py b/gramps/cli/arghandler.py index eac93953a..1d5575bb5 100644 --- a/gramps/cli/arghandler.py +++ b/gramps/cli/arghandler.py @@ -54,7 +54,6 @@ from .clidbman import CLIDbManager, NAME_FILE, find_locker_name from gramps.gen.plug import BasePluginManager from gramps.gen.plug.report import CATEGORY_BOOK, CATEGORY_CODE, BookList from .plug import cl_report, cl_book -from .user import User from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext @@ -153,10 +152,15 @@ class ArgHandler(object): def __init__(self, dbstate, parser, sessionmanager, errorfunc=None, gui=False): + from .user import User + self.dbstate = dbstate self.sm = sessionmanager self.errorfunc = errorfunc self.gui = gui + self.user = User(error=self.__error, + auto_accept=parser.auto_accept, + quiet=parser.quiet) if self.gui: self.actions = [] self.list = False @@ -283,19 +287,12 @@ class ArgHandler(object): else: fullpath = os.path.abspath(os.path.expanduser(fname)) if os.path.exists(fullpath): - self.__error(_("WARNING: Output file already exists!\n" - "WARNING: It will be overwritten:\n %s") % - fullpath) - try: - if sys.version_info[0] < 3: - ask = raw_input - else: - ask = input - answer = ask(_('OK to overwrite? (yes/no) ')) - except EOFError: - print() - sys.exit(0) - if answer.upper() in ('Y', 'YES', _('YES').upper()): + message = _("WARNING: Output file already exists!\n" + "WARNING: It will be overwritten:\n %s" + ) % fullpath + accepted = self.user.prompt(_('OK to overwrite?'), message, + _('yes'), _('no')) + if accepted: self.__error(_("Will overwrite the existing file: %s") % fullpath) else: @@ -551,7 +548,7 @@ class ArgHandler(object): for plugin in pmgr.get_import_plugins(): if family_tree_format == plugin.get_extension(): import_function = plugin.get_import_function() - import_function(self.dbstate.db, filename, User()) + import_function(self.dbstate.db, filename, self.user) if not self.cl: if self.imp_db_path: @@ -573,7 +570,7 @@ class ArgHandler(object): for plugin in pmgr.get_export_plugins(): if family_tree_format == plugin.get_extension(): export_function = plugin.get_export_function() - export_function(self.dbstate.db, filename, User(error=self.__error)) + export_function(self.dbstate.db, filename, self.user) #------------------------------------------------------------------------- # diff --git a/gramps/cli/argparser.py b/gramps/cli/argparser.py index 00078097e..b6b894bae 100644 --- a/gramps/cli/argparser.py +++ b/gramps/cli/argparser.py @@ -53,7 +53,6 @@ from gramps.gen.utils.file import get_unicode_path_from_env_var from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext -# Note: Make sure to edit const.py.in POPT_TABLE too! _HELP = _(""" Usage: gramps.py [OPTION...] --load-modules=MODULE1,MODULE2,... Dynamic modules to load @@ -77,6 +76,8 @@ Application options -u, --force-unlock Force unlock of Family Tree -s, --show Show config settings -c, --config=[config.setting[:value]] Set config setting(s) and start Gramps + -y, --yes Don't ask to confirm dangerous actions (non-GUI mode only) + -q, --quiet Suppress progress indication output (non-GUI mode only) -v, --version Show versions """) @@ -183,6 +184,8 @@ class ArgParser(object): self.force_unlock = False self.create = None self.runqml = False + self.quiet = False + self.auto_accept = False self.errors = [] self.parse_args() @@ -198,19 +201,21 @@ class ArgParser(object): Possible: 1/ Just the family tree (name or database dir) - 2/ -O, --open: Open of a family tree - 3/ -i, --import: Import a family tree of any format understood by + 2/ -O --open: Open of a family tree + 3/ -i --import: Import a family tree of any format understood by an importer, optionally provide -f to indicate format - 4/ -e, --export: export a family tree in required format, optionally + 4/ -e --export: export a family tree in required format, optionally provide -f to indicate format - 5/ -f, --format=FORMAT : format after a -i or -e option - 6/ -a, --action: An action (possible: 'report', 'tool') - 7/ -p, --options=OPTIONS_STRING : specify options - 8/ -u, --force-unlock: A locked database can be unlocked by giving + 5/ -f --format=FORMAT : format after a -i or -e option + 6/ -a --action: An action (possible: 'report', 'tool') + 7/ -p --options=OPTIONS_STRING : specify options + 8/ -u --force-unlock: A locked database can be unlocked by giving this argument when opening it 9/ -s --show : Show config settings 10/ -c --config=config.setting:value : Set config.setting and start Gramps without :value, the actual config.setting is shown + 11/ -y --yes: assume user's acceptance of any CLI prompt (see cli.user.User.prompt) + 12/ -q --quiet: suppress extra noise on sys.stderr, such as progress indicators """ try: @@ -259,23 +264,23 @@ class ArgParser(object): need_to_quit = False for opt_ix in range(len(options)): option, value = options[opt_ix] - if option in ( '-O', '--open'): + if option in ['-O', '--open']: self.open = value - elif option in ( '-C', '--create'): + elif option in ['-C', '--create']: self.create = value - elif option in ( '-i', '--import'): + elif option in ['-i', '--import']: family_tree_format = None if opt_ix < len(options) - 1 \ and options[opt_ix + 1][0] in ( '-f', '--format'): family_tree_format = options[opt_ix + 1][1] self.imports.append((value, family_tree_format)) - elif option in ( '-e', '--export' ): + elif option in ['-e', '--export']: family_tree_format = None if opt_ix < len(options) - 1 \ and options[opt_ix + 1][0] in ( '-f', '--format'): family_tree_format = options[opt_ix + 1][1] self.exports.append((value, family_tree_format)) - elif option in ( '-a', '--action' ): + elif option in ['-a', '--action']: action = value if action not in ('report', 'tool', 'book'): print(_("Unknown action: %s. Ignoring.") % action, @@ -286,18 +291,18 @@ class ArgParser(object): and options[opt_ix+1][0] in ( '-p', '--options' ): options_str = options[opt_ix+1][1] self.actions.append((action, options_str)) - elif option in ('-d', '--debug'): + elif option in ['-d', '--debug']: print(_('setup debugging'), value, file=sys.stderr) logger = logging.getLogger(value) logger.setLevel(logging.DEBUG) cleandbg += [opt_ix] - elif option in ('-l'): + elif option in ['-l']: self.list = True - elif option in ('-L'): + elif option in ['-L']: self.list_more = True - elif option in ('-t'): + elif option in ['-t']: self.list_table = True - elif option in ('-s','--show'): + elif option in ['-s','--show']: print(_("Gramps config settings from %s:") % config.filename) for section in config.data: @@ -307,7 +312,7 @@ class ArgParser(object): repr(config.data[section][setting]))) print() sys.exit(0) - elif option in ('-c', '--config'): + elif option in ['-c', '--config']: setting_name = value set_value = False if setting_name: @@ -339,14 +344,18 @@ class ArgParser(object): % setting_name, file=sys.stderr) need_to_quit = True cleandbg += [opt_ix] - elif option in ('-h', '-?', '--help'): + elif option in ['-h', '-?', '--help']: self.help = True - elif option in ('-u', '--force-unlock'): + elif option in ['-u', '--force-unlock']: self.force_unlock = True - elif option in ('--usage'): + elif option in ['--usage']: self.usage = True - elif option in ('--qml'): + elif option in ['--qml']: self.runqml = True + elif option in ['-y', '--yes']: + self.auto_accept = True + elif option in ['-q', '--quiet']: + self.quiet = True #clean options list cleandbg.reverse() diff --git a/gramps/cli/test/argparser_test.py b/gramps/cli/test/argparser_test.py new file mode 100644 index 000000000..5f250cc80 --- /dev/null +++ b/gramps/cli/test/argparser_test.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2013 Vassilii Khachaturov +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# $Id$ + +""" Unittest for argparser.py """ + +from __future__ import print_function + +import unittest +from ..argparser import ArgParser +import sys + +try: + if sys.version_info < (3,3): + from mock import Mock + else: + from unittest.mock import Mock + + MOCKING = True + +except: + MOCKING = False + print ("Mocking disabled", sys.exc_info()[0:2]) + +class TestArgParser(unittest.TestCase): + def setUp(self): + pass + + def create_parser(*self_and_args): + return ArgParser(list(self_and_args)) + + def triggers_option_error(self, option): + ap = self.create_parser(option) + return (str(ap.errors).find("option "+option)>=0, ap) + + def test_wrong_argument_triggers_option_error(self): + bad,ap = self.triggers_option_error('--I-am-a-wrong-argument') + assert bad, ap.__dict__ + + def test_y_shortopt_sets_auto_accept(self): + bad,ap = self.triggers_option_error('-y') + assert not bad, ap.errors + assert ap.auto_accept + + def test_yes_longopt_sets_auto_accept(self): + bad,ap = self.triggers_option_error('--yes') + assert not bad, ap.errors + assert ap.auto_accept + + def test_q_shortopt_sets_quiet(self): + bad,ap = self.triggers_option_error('-q') + assert not bad, ap.errors + assert ap.quiet + + def test_quiet_longopt_sets_quiet(self): + bad,ap = self.triggers_option_error('--quiet') + assert not bad, ap.errors + assert ap.quiet + + def test_quiet_exists_by_default(self): + ap = self.create_parser() + assert hasattr(ap,'quiet') + + def test_auto_accept_unset_by_default(self): + ap = self.create_parser() + assert not ap.auto_accept + +if __name__ == "__main__": + unittest.main() diff --git a/gramps/cli/test/user_test.py b/gramps/cli/test/user_test.py index 42335c103..14168f3d4 100644 --- a/gramps/cli/test/user_test.py +++ b/gramps/cli/test/user_test.py @@ -32,9 +32,9 @@ import sys try: if sys.version_info < (3,3): - from mock import Mock + from mock import Mock, NonCallableMock else: - from unittest.mock import Mock + from unittest.mock import Mock, NonCallableMock MOCKING = True @@ -47,8 +47,8 @@ class TestUser_prompt(unittest.TestCase): self.real_user = user.User() if MOCKING: self.user = user.User() - self.user._fileout = Mock() - self.user._input = Mock() + self.user._fileout = Mock(spec=sys.stderr) + self.user._input = Mock(spec=input) def test_default_fileout_has_write(self): assert hasattr(self.real_user._fileout, 'write') @@ -100,10 +100,45 @@ class TestUser_prompt(unittest.TestCase): self.assert_prompt_contains_text(TestUser.REJECT) if not MOCKING: #don't use SKIP, to avoid counting a skipped test - def testManualRun(self): + def test_manual_run(self): b = self.real_user.prompt( TestUser.TITLE, TestUser.MSG, TestUser.ACCEPT, TestUser.REJECT) print ("Returned: {}".format(b)) + @unittest.skipUnless(MOCKING, "Requires unittest.mock to run") + def test_auto_accept_accepts_without_prompting(self): + u = user.User(auto_accept=True) + u._fileout = Mock(spec=sys.stderr) + assert u.prompt( + TestUser.TITLE, TestUser.MSG, TestUser.ACCEPT, TestUser.REJECT + ), "True expected!" + assert len(u._fileout.method_calls) == 0, list(u._fileout.method_calls) + + @unittest.skipUnless(MOCKING, "Requires unittest.mock to run") + def test_EOFError_in_prompt_caught_as_False(self): + self.user._input.configure_mock( + side_effect = EOFError, + return_value = TestUser.REJECT) + assert not self.user.prompt( + TestUser.TITLE, TestUser.MSG, TestUser.ACCEPT, TestUser.REJECT + ), "False expected!" + self.user._input.assert_called_once_with() + +@unittest.skipUnless(MOCKING, "Requires unittest.mock to run") +class TestUser_quiet(unittest.TestCase): + def setUp(self): + self.user = user.User(quiet=True) + self.user._fileout = Mock(spec=sys.stderr) + + def test_progress_can_begin_step_end(self): + self.user.begin_progress("Foo", "Bar", 0) + for i in range(10): + self.user.step_progress() + self.user.end_progress() + + def tearDown(self): + assert len(self.user._fileout.method_calls + ) == 0, list(self.user._fileout.method_calls) + if __name__ == "__main__": unittest.main() diff --git a/gramps/cli/user.py b/gramps/cli/user.py index e238b5e0a..cd0326a38 100644 --- a/gramps/cli/user.py +++ b/gramps/cli/user.py @@ -58,7 +58,7 @@ class User(user.User): This class provides a means to interact with the user via CLI. It implements the interface in gramps.gen.user.User() """ - def __init__(self, callback=None, error=None): + def __init__(self, callback=None, error=None, auto_accept=False, quiet=False): """ Init. @@ -69,6 +69,15 @@ class User(user.User): self.steps = 0; self.current_step = 0; self._input = raw_input if sys.version_info[0] < 3 else input + + def yes(*args): + return True + + if auto_accept: + self.prompt = yes + if quiet: + self.begin_progress = self.end_progress = self.step_progress = \ + self._default_callback = yes def begin_progress(self, title, message, steps): """ @@ -127,13 +136,17 @@ class User(user.User): @returns: the user's answer to the question @rtype: bool """ - text = "{t} {m} ({y}/{n}): ".format( + text = "{t}\n{m} ([{y}]/{n}): ".format( t = title, m = message, y = accept_label, n = reject_label) print (text, file = self._fileout) # TODO python3 add flush=True - return self._input() == accept_label + try: + reply = self._input() + return reply == "" or reply == accept_label + except EOFError: + return False def warn(self, title, warning=""): """ diff --git a/gramps/gen/const.py b/gramps/gen/const.py index 9c817d132..b5de21970 100644 --- a/gramps/gen/const.py +++ b/gramps/gen/const.py @@ -303,8 +303,10 @@ LONGOPTS = [ "usage", "version", "qml", + "yes", + "quiet", ] -SHORTOPTS = "O:C:i:e:f:a:p:d:c:lLthuv?s" +SHORTOPTS = "O:C:i:e:f:a:p:d:c:lLthuv?syq" GRAMPS_UUID = uuid.UUID('516cd010-5a41-470f-99f8-eb22f1098ad6') diff --git a/gramps/gen/user.py b/gramps/gen/user.py index b6229de3f..cb6975bb1 100644 --- a/gramps/gen/user.py +++ b/gramps/gen/user.py @@ -69,10 +69,13 @@ class User(): else: self.callback_function(percentage) else: - if text is None: - self._fileout.write("\r%02d%%" % percentage) - else: - self._fileout.write("\r%02d%% %s" % (percentage, text)) + self._default_callback(percentage, text) + + def _default_callback(self, percentage, text): + if text is None: + self._fileout.write("\r%02d%%" % percentage) + else: + self._fileout.write("\r%02d%% %s" % (percentage, text)) def end_progress(self): """ diff --git a/test/impex.sh b/test/impex.sh index 95fd9fabe..25df87746 100755 --- a/test/impex.sh +++ b/test/impex.sh @@ -16,7 +16,7 @@ TOP_DIR=`dirname $PWD` TEST_DIR=$TOP_DIR/test SRC_DIR=$TOP_DIR/gramps -PRG="python ../Gramps.py" +PRG="python ../Gramps.py --yes --quiet" EXAMPLE_XML=$TOP_DIR/example/gramps/example.gramps OUT_FMT="csv ged gramps gpkg wft gw vcs vcf"