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.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)
#-------------------------------------------------------------------------
#

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
_ = 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()

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:
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()

View File

@ -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.
@ -70,6 +70,15 @@ class User(user.User):
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):
"""
Start showing a progress indicator to the user.
@ -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=""):
"""

View File

@ -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')

View File

@ -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):
"""

View File

@ -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"