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
This commit is contained in:
Vassilii Khachaturov 2013-08-28 09:24:26 +00:00
parent 1bb6398717
commit 62854bb089
8 changed files with 200 additions and 53 deletions

View File

@ -54,7 +54,6 @@ from .clidbman import CLIDbManager, NAME_FILE, find_locker_name
from gramps.gen.plug import BasePluginManager from gramps.gen.plug import BasePluginManager
from gramps.gen.plug.report import CATEGORY_BOOK, CATEGORY_CODE, BookList from gramps.gen.plug.report import CATEGORY_BOOK, CATEGORY_CODE, BookList
from .plug import cl_report, cl_book from .plug import cl_report, cl_book
from .user import User
from gramps.gen.const import GRAMPS_LOCALE as glocale from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext _ = glocale.translation.gettext
@ -153,10 +152,15 @@ class ArgHandler(object):
def __init__(self, dbstate, parser, sessionmanager, def __init__(self, dbstate, parser, sessionmanager,
errorfunc=None, gui=False): errorfunc=None, gui=False):
from .user import User
self.dbstate = dbstate self.dbstate = dbstate
self.sm = sessionmanager self.sm = sessionmanager
self.errorfunc = errorfunc self.errorfunc = errorfunc
self.gui = gui self.gui = gui
self.user = User(error=self.__error,
auto_accept=parser.auto_accept,
quiet=parser.quiet)
if self.gui: if self.gui:
self.actions = [] self.actions = []
self.list = False self.list = False
@ -283,19 +287,12 @@ class ArgHandler(object):
else: else:
fullpath = os.path.abspath(os.path.expanduser(fname)) fullpath = os.path.abspath(os.path.expanduser(fname))
if os.path.exists(fullpath): if os.path.exists(fullpath):
self.__error(_("WARNING: Output file already exists!\n" message = _("WARNING: Output file already exists!\n"
"WARNING: It will be overwritten:\n %s") % "WARNING: It will be overwritten:\n %s"
fullpath) ) % fullpath
try: accepted = self.user.prompt(_('OK to overwrite?'), message,
if sys.version_info[0] < 3: _('yes'), _('no'))
ask = raw_input if accepted:
else:
ask = input
answer = ask(_('OK to overwrite? (yes/no) '))
except EOFError:
print()
sys.exit(0)
if answer.upper() in ('Y', 'YES', _('YES').upper()):
self.__error(_("Will overwrite the existing file: %s") self.__error(_("Will overwrite the existing file: %s")
% fullpath) % fullpath)
else: else:
@ -551,7 +548,7 @@ class ArgHandler(object):
for plugin in pmgr.get_import_plugins(): for plugin in pmgr.get_import_plugins():
if family_tree_format == plugin.get_extension(): if family_tree_format == plugin.get_extension():
import_function = plugin.get_import_function() 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 not self.cl:
if self.imp_db_path: if self.imp_db_path:
@ -573,7 +570,7 @@ class ArgHandler(object):
for plugin in pmgr.get_export_plugins(): for plugin in pmgr.get_export_plugins():
if family_tree_format == plugin.get_extension(): if family_tree_format == plugin.get_extension():
export_function = plugin.get_export_function() export_function = plugin.get_export_function()
export_function(self.dbstate.db, filename, User(error=self.__error)) export_function(self.dbstate.db, filename, self.user)
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #

View File

@ -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 from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext _ = glocale.translation.gettext
# Note: Make sure to edit const.py.in POPT_TABLE too!
_HELP = _(""" _HELP = _("""
Usage: gramps.py [OPTION...] Usage: gramps.py [OPTION...]
--load-modules=MODULE1,MODULE2,... Dynamic modules to load --load-modules=MODULE1,MODULE2,... Dynamic modules to load
@ -77,6 +76,8 @@ Application options
-u, --force-unlock Force unlock of Family Tree -u, --force-unlock Force unlock of Family Tree
-s, --show Show config settings -s, --show Show config settings
-c, --config=[config.setting[:value]] Set config setting(s) and start Gramps -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 -v, --version Show versions
""") """)
@ -183,6 +184,8 @@ class ArgParser(object):
self.force_unlock = False self.force_unlock = False
self.create = None self.create = None
self.runqml = False self.runqml = False
self.quiet = False
self.auto_accept = False
self.errors = [] self.errors = []
self.parse_args() self.parse_args()
@ -198,19 +201,21 @@ class ArgParser(object):
Possible: Possible:
1/ Just the family tree (name or database dir) 1/ Just the family tree (name or database dir)
2/ -O, --open: Open of a family tree 2/ -O --open: Open of a family tree
3/ -i, --import: Import a family tree of any format understood by 3/ -i --import: Import a family tree of any format understood by
an importer, optionally provide -f to indicate format 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 provide -f to indicate format
5/ -f, --format=FORMAT : format after a -i or -e option 5/ -f --format=FORMAT : format after a -i or -e option
6/ -a, --action: An action (possible: 'report', 'tool') 6/ -a --action: An action (possible: 'report', 'tool')
7/ -p, --options=OPTIONS_STRING : specify options 7/ -p --options=OPTIONS_STRING : specify options
8/ -u, --force-unlock: A locked database can be unlocked by giving 8/ -u --force-unlock: A locked database can be unlocked by giving
this argument when opening it this argument when opening it
9/ -s --show : Show config settings 9/ -s --show : Show config settings
10/ -c --config=config.setting:value : Set config.setting and start 10/ -c --config=config.setting:value : Set config.setting and start
Gramps without :value, the actual config.setting is shown 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: try:
@ -259,23 +264,23 @@ class ArgParser(object):
need_to_quit = False need_to_quit = False
for opt_ix in range(len(options)): for opt_ix in range(len(options)):
option, value = options[opt_ix] option, value = options[opt_ix]
if option in ( '-O', '--open'): if option in ['-O', '--open']:
self.open = value self.open = value
elif option in ( '-C', '--create'): elif option in ['-C', '--create']:
self.create = value self.create = value
elif option in ( '-i', '--import'): elif option in ['-i', '--import']:
family_tree_format = None family_tree_format = None
if opt_ix < len(options) - 1 \ if opt_ix < len(options) - 1 \
and options[opt_ix + 1][0] in ( '-f', '--format'): and options[opt_ix + 1][0] in ( '-f', '--format'):
family_tree_format = options[opt_ix + 1][1] family_tree_format = options[opt_ix + 1][1]
self.imports.append((value, family_tree_format)) self.imports.append((value, family_tree_format))
elif option in ( '-e', '--export' ): elif option in ['-e', '--export']:
family_tree_format = None family_tree_format = None
if opt_ix < len(options) - 1 \ if opt_ix < len(options) - 1 \
and options[opt_ix + 1][0] in ( '-f', '--format'): and options[opt_ix + 1][0] in ( '-f', '--format'):
family_tree_format = options[opt_ix + 1][1] family_tree_format = options[opt_ix + 1][1]
self.exports.append((value, family_tree_format)) self.exports.append((value, family_tree_format))
elif option in ( '-a', '--action' ): elif option in ['-a', '--action']:
action = value action = value
if action not in ('report', 'tool', 'book'): if action not in ('report', 'tool', 'book'):
print(_("Unknown action: %s. Ignoring.") % action, print(_("Unknown action: %s. Ignoring.") % action,
@ -286,18 +291,18 @@ class ArgParser(object):
and options[opt_ix+1][0] in ( '-p', '--options' ): and options[opt_ix+1][0] in ( '-p', '--options' ):
options_str = options[opt_ix+1][1] options_str = options[opt_ix+1][1]
self.actions.append((action, options_str)) self.actions.append((action, options_str))
elif option in ('-d', '--debug'): elif option in ['-d', '--debug']:
print(_('setup debugging'), value, file=sys.stderr) print(_('setup debugging'), value, file=sys.stderr)
logger = logging.getLogger(value) logger = logging.getLogger(value)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
cleandbg += [opt_ix] cleandbg += [opt_ix]
elif option in ('-l'): elif option in ['-l']:
self.list = True self.list = True
elif option in ('-L'): elif option in ['-L']:
self.list_more = True self.list_more = True
elif option in ('-t'): elif option in ['-t']:
self.list_table = True self.list_table = True
elif option in ('-s','--show'): elif option in ['-s','--show']:
print(_("Gramps config settings from %s:") print(_("Gramps config settings from %s:")
% config.filename) % config.filename)
for section in config.data: for section in config.data:
@ -307,7 +312,7 @@ class ArgParser(object):
repr(config.data[section][setting]))) repr(config.data[section][setting])))
print() print()
sys.exit(0) sys.exit(0)
elif option in ('-c', '--config'): elif option in ['-c', '--config']:
setting_name = value setting_name = value
set_value = False set_value = False
if setting_name: if setting_name:
@ -339,14 +344,18 @@ class ArgParser(object):
% setting_name, file=sys.stderr) % setting_name, file=sys.stderr)
need_to_quit = True need_to_quit = True
cleandbg += [opt_ix] cleandbg += [opt_ix]
elif option in ('-h', '-?', '--help'): elif option in ['-h', '-?', '--help']:
self.help = True self.help = True
elif option in ('-u', '--force-unlock'): elif option in ['-u', '--force-unlock']:
self.force_unlock = True self.force_unlock = True
elif option in ('--usage'): elif option in ['--usage']:
self.usage = True self.usage = True
elif option in ('--qml'): elif option in ['--qml']:
self.runqml = True self.runqml = True
elif option in ['-y', '--yes']:
self.auto_accept = True
elif option in ['-q', '--quiet']:
self.quiet = True
#clean options list #clean options list
cleandbg.reverse() cleandbg.reverse()

View File

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2013 Vassilii Khachaturov <vassilii@tarunz.org>
#
# 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()

View File

@ -32,9 +32,9 @@ import sys
try: try:
if sys.version_info < (3,3): if sys.version_info < (3,3):
from mock import Mock from mock import Mock, NonCallableMock
else: else:
from unittest.mock import Mock from unittest.mock import Mock, NonCallableMock
MOCKING = True MOCKING = True
@ -47,8 +47,8 @@ class TestUser_prompt(unittest.TestCase):
self.real_user = user.User() self.real_user = user.User()
if MOCKING: if MOCKING:
self.user = user.User() self.user = user.User()
self.user._fileout = Mock() self.user._fileout = Mock(spec=sys.stderr)
self.user._input = Mock() self.user._input = Mock(spec=input)
def test_default_fileout_has_write(self): def test_default_fileout_has_write(self):
assert hasattr(self.real_user._fileout, 'write') assert hasattr(self.real_user._fileout, 'write')
@ -100,10 +100,45 @@ class TestUser_prompt(unittest.TestCase):
self.assert_prompt_contains_text(TestUser.REJECT) self.assert_prompt_contains_text(TestUser.REJECT)
if not MOCKING: #don't use SKIP, to avoid counting a skipped test 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( b = self.real_user.prompt(
TestUser.TITLE, TestUser.MSG, TestUser.ACCEPT, TestUser.REJECT) TestUser.TITLE, TestUser.MSG, TestUser.ACCEPT, TestUser.REJECT)
print ("Returned: {}".format(b)) 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__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -58,7 +58,7 @@ class User(user.User):
This class provides a means to interact with the user via CLI. This class provides a means to interact with the user via CLI.
It implements the interface in gramps.gen.user.User() 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. Init.
@ -70,6 +70,15 @@ class User(user.User):
self.current_step = 0; self.current_step = 0;
self._input = raw_input if sys.version_info[0] < 3 else input 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): def begin_progress(self, title, message, steps):
""" """
Start showing a progress indicator to the user. Start showing a progress indicator to the user.
@ -127,13 +136,17 @@ class User(user.User):
@returns: the user's answer to the question @returns: the user's answer to the question
@rtype: bool @rtype: bool
""" """
text = "{t} {m} ({y}/{n}): ".format( text = "{t}\n{m} ([{y}]/{n}): ".format(
t = title, t = title,
m = message, m = message,
y = accept_label, y = accept_label,
n = reject_label) n = reject_label)
print (text, file = self._fileout) # TODO python3 add flush=True 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=""): def warn(self, title, warning=""):
""" """

View File

@ -303,8 +303,10 @@ LONGOPTS = [
"usage", "usage",
"version", "version",
"qml", "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') GRAMPS_UUID = uuid.UUID('516cd010-5a41-470f-99f8-eb22f1098ad6')

View File

@ -69,6 +69,9 @@ class User():
else: else:
self.callback_function(percentage) self.callback_function(percentage)
else: else:
self._default_callback(percentage, text)
def _default_callback(self, percentage, text):
if text is None: if text is None:
self._fileout.write("\r%02d%%" % percentage) self._fileout.write("\r%02d%%" % percentage)
else: else:

View File

@ -16,7 +16,7 @@
TOP_DIR=`dirname $PWD` TOP_DIR=`dirname $PWD`
TEST_DIR=$TOP_DIR/test TEST_DIR=$TOP_DIR/test
SRC_DIR=$TOP_DIR/gramps SRC_DIR=$TOP_DIR/gramps
PRG="python ../Gramps.py" PRG="python ../Gramps.py --yes --quiet"
EXAMPLE_XML=$TOP_DIR/example/gramps/example.gramps EXAMPLE_XML=$TOP_DIR/example/gramps/example.gramps
OUT_FMT="csv ged gramps gpkg wft gw vcs vcf" OUT_FMT="csv ged gramps gpkg wft gw vcs vcf"