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:
parent
1bb6398717
commit
62854bb089
@ -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)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
|
@ -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()
|
||||
|
88
gramps/cli/test/argparser_test.py
Normal file
88
gramps/cli/test/argparser_test.py
Normal 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()
|
@ -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()
|
||||
|
@ -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=""):
|
||||
"""
|
||||
|
@ -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')
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user