From 62854bb089aba17e4906289d800f2c913cbccb75 Mon Sep 17 00:00:00 2001 From: Vassilii Khachaturov Date: Wed, 28 Aug 2013 09:24:26 +0000 Subject: [PATCH] 7016: new cmdline switches -y/--yes and -q/--quiet Add to -h output new text about -y and -q impex.sh switched to use --yes and --quiet Refactor ArgHandler to reuse User object ArgHandler now uses user.prompt No longer custom code duplicating user.prompt functionality This dropped support for English yes/no and prefixes in the "OK to overwrite?", as User.prompt allows pressing "Enter" to accept by default, and everything else except verbatim accept choice will be treated as reject. cli.user.User.prompt now supports treating EOF as a reject prompt message reformatted: added newline after title Previously, code '-q' in ('--qml') returned True, which was not what ArgParser meant. Changed the rhs of in to [] from () to avoid this for every case in ArgParser.parse in the future as well. Tests run: the new UT added and impex.sh svn: r22916 --- gramps/cli/arghandler.py | 29 +++++----- gramps/cli/argparser.py | 55 +++++++++++-------- gramps/cli/test/argparser_test.py | 88 +++++++++++++++++++++++++++++++ gramps/cli/test/user_test.py | 45 ++++++++++++++-- gramps/cli/user.py | 19 +++++-- gramps/gen/const.py | 4 +- gramps/gen/user.py | 11 ++-- test/impex.sh | 2 +- 8 files changed, 200 insertions(+), 53 deletions(-) create mode 100644 gramps/cli/test/argparser_test.py 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"