Merge lp:~vila/bzr/bool-config-option into lp:~bzr/bzr/trunk-old

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Ian Clatworthy
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~vila/bzr/bool-config-option
Merge into: lp:~bzr/bzr/trunk-old
Diff against target: 312 lines
To merge this branch: bzr merge lp:~vila/bzr/bool-config-option
Reviewer Review Type Date Requested Status
Ian Clatworthy Approve
Review via email: mp+8180@code.launchpad.net

This proposal supersedes a proposal from 2009-07-02.

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal

This patch adds the ability to interpret a configuration as a boolean.

It can be viewed (or reviewed :) as:
- add a bool_from_string() function,
- use that function to define config.get_user_option_as_bool()
- use that function in ui.get_boolean
- use config.get_user_option_as_bool() where needed

Regarding the discussion about defining a new method when we add a new configuration variable,
my opinion is that it's not always worth it.

The --strict option for push and send can be defined anywhere, I prefer to handle the
default value in the commands themselves because:
- that's the only place they are used,
- that's the place that is targeted by the tests

Revision history for this message
John A Meinel (jameinel) wrote : Posted in a previous version of this proposal

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Vincent Ladeuil wrote:
> Vincent Ladeuil has proposed merging lp:~vila/bzr/bool-config-option into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
>
> This patch adds the ability to interpret a configuration as a boolean.
>
> It can be viewed (or reviewed :) as:
> - add a bool_from_string() function,
> - use that function to define config.get_user_option_as_bool()
> - use that function in ui.get_boolean
> - use config.get_user_option_as_bool() where needed
>
> Regarding the discussion about defining a new method when we add a new configuration variable,
> my opinion is that it's not always worth it.
>
> The --strict option for push and send can be defined anywhere, I prefer to handle the
> default value in the commands themselves because:
> - that's the only place they are used,
> - that's the place that is targeted by the tests
>

Though it also makes it hard to know the full list of config items that
one could possibly set without using "grep -rnI get_user_option bzrlib"

Anyway, this patch would probably be fine if lp:mad wasn't crazy.

  review resubmit

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkpM5tQACgkQJdeBCYSNAAPYeACfbcQUsCFWHvP5zJlzbXOtYr4r
idgAn3xS0cIKF70naJYUDm/4tv87C1tD
=O5+R
-----END PGP SIGNATURE-----

review: Needs Resubmitting
Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

Looks all ok to me. I'd like to suggest though that the rstrip('\n') call in ui/__init__.py omit the parameter and default to stripping all trailing whitespace. That might work better on Windows? (Either way, I think trailing whitespace is harmless.)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/builtins.py'
2--- bzrlib/builtins.py 2009-07-02 21:17:35 +0000
3+++ bzrlib/builtins.py 2009-07-09 07:35:50 +0000
4@@ -1096,24 +1096,15 @@
5 (tree, br_from,
6 _unused) = bzrdir.BzrDir.open_containing_tree_or_branch(directory)
7 if strict is None:
8- strict = br_from.get_config().get_user_option('push_strict')
9- if strict is not None:
10- # FIXME: This should be better supported by config
11- # -- vila 20090611
12- bools = dict(yes=True, no=False, on=True, off=False,
13- true=True, false=False)
14- try:
15- strict = bools[strict.lower()]
16- except KeyError:
17- strict = None
18+ strict = br_from.get_config().get_user_option_as_bool('push_strict')
19+ if strict is None: strict = True # default value
20 # Get the tip's revision_id
21 revision = _get_one_revision('push', revision)
22 if revision is not None:
23 revision_id = revision.in_history(br_from).rev_id
24 else:
25 revision_id = None
26- if (tree is not None and revision_id is None
27- and (strict is None or strict)): # Default to True:
28+ if strict and tree is not None and revision_id is None:
29 changes = tree.changes_from(tree.basis_tree())
30 if changes.has_changed() or len(tree.get_parent_ids()) > 1:
31 raise errors.UncommittedChanges(
32
33=== modified file 'bzrlib/config.py'
34--- bzrlib/config.py 2009-06-11 06:49:21 +0000
35+++ bzrlib/config.py 2009-07-09 07:35:50 +0000
36@@ -146,6 +146,9 @@
37 class Config(object):
38 """A configuration policy - what username, editor, gpg needs etc."""
39
40+ def __init__(self):
41+ super(Config, self).__init__()
42+
43 def get_editor(self):
44 """Get the users pop up editor."""
45 raise NotImplementedError
46@@ -174,6 +177,15 @@
47 """Get a generic option - no special process, no default."""
48 return self._get_user_option(option_name)
49
50+ def get_user_option_as_bool(self, option_name):
51+ """Get a generic option as a boolean - no special process, no default.
52+
53+ :return None if the option doesn't exist or its value can't be
54+ interpreted as a boolean. Returns True or False ortherwise.
55+ """
56+ s = self._get_user_option(option_name)
57+ return ui.bool_from_string(s)
58+
59 def gpg_signing_command(self):
60 """What program should be used to sign signatures?"""
61 result = self._gpg_signing_command()
62@@ -196,9 +208,6 @@
63 """See log_format()."""
64 return None
65
66- def __init__(self):
67- super(Config, self).__init__()
68-
69 def post_commit(self):
70 """An ordered list of python functions to call.
71
72@@ -299,6 +308,11 @@
73 class IniBasedConfig(Config):
74 """A configuration policy that draws from ini files."""
75
76+ def __init__(self, get_filename):
77+ super(IniBasedConfig, self).__init__()
78+ self._get_filename = get_filename
79+ self._parser = None
80+
81 def _get_parser(self, file=None):
82 if self._parser is not None:
83 return self._parser
84@@ -381,11 +395,6 @@
85 """See Config.log_format."""
86 return self._get_user_option('log_format')
87
88- def __init__(self, get_filename):
89- super(IniBasedConfig, self).__init__()
90- self._get_filename = get_filename
91- self._parser = None
92-
93 def _post_commit(self):
94 """See Config.post_commit."""
95 return self._get_user_option('post_commit')
96
97=== modified file 'bzrlib/send.py'
98--- bzrlib/send.py 2009-07-01 15:17:33 +0000
99+++ bzrlib/send.py 2009-07-09 07:35:50 +0000
100@@ -108,17 +108,10 @@
101 base_revision_id = revision[0].as_revision_id(branch)
102 if revision_id is None:
103 if strict is None:
104- strict = branch.get_config().get_user_option('send_strict')
105- if strict is not None:
106- # FIXME: This should be better supported by config
107- # -- vila 20090626
108- bools = dict(yes=True, no=False, on=True, off=False,
109- true=True, false=False)
110- try:
111- strict = bools[strict.lower()]
112- except KeyError:
113- strict = None
114- if tree is not None and (strict is None or strict):
115+ strict = branch.get_config(
116+ ).get_user_option_as_bool('send_strict')
117+ if strict is None: strict = True # default value
118+ if strict and tree is not None:
119 changes = tree.changes_from(tree.basis_tree())
120 if changes.has_changed() or len(tree.get_parent_ids()) > 1:
121 raise errors.UncommittedChanges(
122
123=== modified file 'bzrlib/tests/test_config.py'
124--- bzrlib/tests/test_config.py 2009-06-29 12:14:09 +0000
125+++ bzrlib/tests/test_config.py 2009-07-09 07:35:50 +0000
126@@ -367,6 +367,20 @@
127 parser = my_config._get_parser(file=config_file)
128 self.failUnless(my_config._get_parser() is parser)
129
130+ def test_get_user_option_as_bool(self):
131+ config_file = StringIO("""
132+a_true_bool = true
133+a_false_bool = 0
134+an_invalid_bool = maybe
135+a_list = hmm, who knows ? # This interpreted as a list !
136+""".encode('utf-8'))
137+ my_config = config.IniBasedConfig(None)
138+ parser = my_config._get_parser(file=config_file)
139+ get_option = my_config.get_user_option_as_bool
140+ self.assertEqual(True, get_option('a_true_bool'))
141+ self.assertEqual(False, get_option('a_false_bool'))
142+ self.assertIs(None, get_option('an_invalid_bool'))
143+ self.assertIs(None, get_option('not_defined_in_this_config'))
144
145 class TestGetConfig(tests.TestCase):
146
147
148=== modified file 'bzrlib/tests/test_ui.py'
149--- bzrlib/tests/test_ui.py 2009-06-30 05:34:47 +0000
150+++ bzrlib/tests/test_ui.py 2009-07-09 07:35:50 +0000
151@@ -174,21 +174,22 @@
152 pb1.finished()
153
154 def assert_get_bool_acceptance_of_user_input(self, factory):
155- factory.stdin = StringIO("y\nyes with garbage\n"
156- "yes\nn\nnot an answer\n"
157- "no\n"
158- "N\nY\n"
159- "foo\n"
160- )
161+ factory.stdin = StringIO("y\n" # True
162+ "n\n" # False
163+ "yes with garbage\nY\n" # True
164+ "not an answer\nno\n" # False
165+ "I'm sure!\nyes\n" # True
166+ "NO\n" # False
167+ "foo\n")
168 factory.stdout = StringIO()
169 factory.stderr = StringIO()
170 # there is no output from the base factory
171 self.assertEqual(True, factory.get_boolean(""))
172- self.assertEqual(True, factory.get_boolean(""))
173- self.assertEqual(False, factory.get_boolean(""))
174- self.assertEqual(False, factory.get_boolean(""))
175- self.assertEqual(False, factory.get_boolean(""))
176- self.assertEqual(True, factory.get_boolean(""))
177+ self.assertEqual(False, factory.get_boolean(""))
178+ self.assertEqual(True, factory.get_boolean(""))
179+ self.assertEqual(False, factory.get_boolean(""))
180+ self.assertEqual(True, factory.get_boolean(""))
181+ self.assertEqual(False, factory.get_boolean(""))
182 self.assertEqual("foo\n", factory.stdin.read())
183 # stdin should be empty
184 self.assertEqual('', factory.stdin.readline())
185@@ -355,3 +356,58 @@
186 r'[####| ] a:b:c 1/2'
187 , uif._progress_view._render_line())
188
189+
190+class TestBoolFromString(tests.TestCase):
191+
192+ def assertIsTrue(self, s, accepted_values=None):
193+ res = _mod_ui.bool_from_string(s, accepted_values=accepted_values)
194+ self.assertEquals(True, res)
195+
196+ def assertIsFalse(self, s, accepted_values=None):
197+ res = _mod_ui.bool_from_string(s, accepted_values=accepted_values)
198+ self.assertEquals(False, res)
199+
200+ def assertIsNone(self, s, accepted_values=None):
201+ res = _mod_ui.bool_from_string(s, accepted_values=accepted_values)
202+ self.assertIs(None, res)
203+
204+ def test_know_valid_values(self):
205+ self.assertIsTrue('true')
206+ self.assertIsFalse('false')
207+ self.assertIsTrue('1')
208+ self.assertIsFalse('0')
209+ self.assertIsTrue('on')
210+ self.assertIsFalse('off')
211+ self.assertIsTrue('yes')
212+ self.assertIsFalse('no')
213+ self.assertIsTrue('y')
214+ self.assertIsFalse('n')
215+ # Also try some case variations
216+ self.assertIsTrue('True')
217+ self.assertIsFalse('False')
218+ self.assertIsTrue('On')
219+ self.assertIsFalse('Off')
220+ self.assertIsTrue('ON')
221+ self.assertIsFalse('OFF')
222+ self.assertIsTrue('oN')
223+ self.assertIsFalse('oFf')
224+
225+ def test_invalid_values(self):
226+ self.assertIsNone(None)
227+ self.assertIsNone('doubt')
228+ self.assertIsNone('frue')
229+ self.assertIsNone('talse')
230+ self.assertIsNone('42')
231+
232+ def test_provided_values(self):
233+ av = dict(y=True, n=False, yes=True, no=False)
234+ self.assertIsTrue('y', av)
235+ self.assertIsTrue('Y', av)
236+ self.assertIsTrue('Yes', av)
237+ self.assertIsFalse('n', av)
238+ self.assertIsFalse('N', av)
239+ self.assertIsFalse('No', av)
240+ self.assertIsNone('1', av)
241+ self.assertIsNone('0', av)
242+ self.assertIsNone('on', av)
243+ self.assertIsNone('off', av)
244
245=== modified file 'bzrlib/ui/__init__.py'
246--- bzrlib/ui/__init__.py 2009-06-30 05:34:47 +0000
247+++ bzrlib/ui/__init__.py 2009-07-09 07:35:50 +0000
248@@ -43,6 +43,42 @@
249 """)
250
251
252+_valid_boolean_strings = dict(yes=True, no=False,
253+ y=True, n=False,
254+ on=True, off=False,
255+ true=True, false=False)
256+_valid_boolean_strings['1'] = True
257+_valid_boolean_strings['0'] = False
258+
259+
260+def bool_from_string(s, accepted_values=None):
261+ """Returns a boolean if the string can be interpreted as such.
262+
263+ Interpret case insensitive strings as booleans. The default values
264+ includes: 'yes', 'no, 'y', 'n', 'true', 'false', '0', '1', 'on',
265+ 'off'. Alternative values can be provided with the 'accepted_values'
266+ parameter.
267+
268+ :param s: A string that should be interpreted as a boolean. It should be of
269+ type string or unicode.
270+
271+ :param accepted_values: An optional dict with accepted strings as keys and
272+ True/False as values. The strings will be tested against a lowered
273+ version of 's'.
274+
275+ :return: True or False for accepted strings, None otherwise.
276+ """
277+ if accepted_values is None:
278+ accepted_values = _valid_boolean_strings
279+ val = None
280+ if type(s) in (str, unicode):
281+ try:
282+ val = accepted_values[s.lower()]
283+ except KeyError:
284+ pass
285+ return val
286+
287+
288 class UIFactory(object):
289 """UI abstraction.
290
291@@ -156,15 +192,16 @@
292 self.stdout = stdout or sys.stdout
293 self.stderr = stderr or sys.stderr
294
295+ _accepted_boolean_strings = dict(y=True, n=False, yes=True, no=False)
296+
297 def get_boolean(self, prompt):
298 while True:
299 self.prompt(prompt + "? [y/n]: ")
300 line = self.stdin.readline()
301- line = line.lower()
302- if line in ('y\n', 'yes\n'):
303- return True
304- if line in ('n\n', 'no\n'):
305- return False
306+ line = line.rstrip('\n')
307+ val = bool_from_string(line, self._accepted_boolean_strings)
308+ if val is not None:
309+ return val
310
311 def get_non_echoed_password(self):
312 isatty = getattr(self.stdin, 'isatty', None)