Merge lp:~mbp/bzr/ui-factory into lp:bzr

Proposed by Martin Pool
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 5426
Proposed branch: lp:~mbp/bzr/ui-factory
Merge into: lp:bzr
Diff against target: 397 lines (+211/-19)
8 files modified
bzrlib/builtins.py (+5/-2)
bzrlib/lockdir.py (+3/-1)
bzrlib/msgeditor.py (+4/-2)
bzrlib/tests/blackbox/test_uncommit.py (+18/-1)
bzrlib/tests/per_uifactory/__init__.py (+23/-0)
bzrlib/tests/test_ui.py (+53/-9)
bzrlib/ui/__init__.py (+68/-1)
doc/developers/ui.txt (+37/-3)
To merge this branch: bzr merge lp:~mbp/bzr/ui-factory
Reviewer Review Type Date Requested Status
Vincent Ladeuil Approve
Andrew Bennetts Needs Fixing
Review via email: mp+34956@code.launchpad.net

Commit message

add structured confirmations to the ui_factory

Description of the change

This is a start towards letting the ui factory know at a more abstract level "we're asking for user confirmation."

This should let us do a few things:

 * systematically skip them when in noninteractive mode
 * let users configure particular confirmations on and off
 * possibly better test them
 * perhaps present the messages differently in guis

Before landing I want to update the callers but I'd appreciate feedback now on the api.

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

117 + def confirm_action(self, prompt, confirmation_id, args, default=True):

s/args/kwargs/

or even **kwargs like the others, there should be a way to make
this compatible with 'default=True' (which isn't documented nor
used by the way).

Oh wait, yes it *is* used in other implementations, ok.

Hmm, I think that 'confirm' already implies that the default is
True so this may be useless. Do you have a use case for
default=False ?

Adding confirm_action to uis sounds like a good idea, but this
needs to be discussed with the qbzr/bzr-gtk people since they
need a way to override it without calling get_boolean for which
they probably also defines aspecific version...

This may not be a bzrlib problem though...

As mentionned on IRC, I like the direction this is taking
regarding make_test_ui_factory being a good step towards a
fixture independent from the test itself.

Regarding 'bzr.lock.break.confirm', I'm fine with using an
arbitrary name space but sticking to the python module name space
may be better suited in the long term (easier to discover and to maintain).

Now, reagrding the relationships with config variables, we
can (hypothetically):

- define an alias between bzr and bzrlib or
  bzr.lock.break.confirm and bzrlib.lock.confirm_break or,
  even simpler, between bzrlib.lock.confirm_break and confirm_break_lock

- define (handwaving) a config var as bzrlib.lock.confirm_break
  and import it in bzrlib.lockdir

Or any combination.

Anyway, I now better understand what you mean for the application
code and indeed this looks nice.

Making the confirm_action implementation access the relevant
config from the ID may still be a big tricky though (wt, branch
?).

This may be addressed by setting some config attribute to the ui
though... unless we need the context manager for that...

Revision history for this message
Martin Pool (mbp) wrote :

On 10 September 2010 02:19, Vincent Ladeuil <email address hidden> wrote:
> 117     + def confirm_action(self, prompt, confirmation_id, args, default=True):
>
> s/args/kwargs/
>
> or even **kwargs like the others, there should be a way to make
> this compatible with 'default=True' (which isn't documented nor
> used by the way).

Thanks for the speedy review.

At the moment I really dislike using **kwargs as shorthand for a dict
to be passed to lower level code, eg to be passed in to string
interpolating. It makes things slightly easier on the caller it's
true, but it also tends to break by clashing with other arguments used
either at this level or in different implementations or as the code
changes later on.

> Oh wait, yes it *is* used in other implementations, ok.
>
> Hmm, I think that 'confirm' already implies that the default is
> True so this may be useless. Do you have a use case for
> default=False ?

That would imply an operation where we want confirmation from a user
and we won't proceed in non-interactive mode, which is conceivable but
unlikely. So perhaps I should just delete it.

>
> Adding confirm_action to uis sounds like a good idea, but this
> needs to be discussed with the qbzr/bzr-gtk people since they
> need a way to override it without calling get_boolean for which
> they probably also defines aspecific version...
>
> This may not be a bzrlib problem though...

I'll cc them and they may have a thought.

> As mentionned on IRC, I like the direction this is taking
> regarding make_test_ui_factory being a good step towards a
> fixture independent from the test itself.
>
> Regarding 'bzr.lock.break.confirm', I'm fine with using an
> arbitrary name space but sticking to the python module name space
> may be better suited in the long term (easier to discover and to maintain).

So bzrlib.lockdir.break, or something similar?

> Now, reagrding the relationships with config variables, we
> can (hypothetically):
>
> - define an alias between bzr and bzrlib or
>  bzr.lock.break.confirm and bzrlib.lock.confirm_break or,
>  even simpler, between bzrlib.lock.confirm_break and confirm_break_lock
>
> - define (handwaving) a config var as bzrlib.lock.confirm_break
>  and import it in bzrlib.lockdir
>
> Or any combination.
>
> Anyway, I now better understand what you mean for the application
> code and indeed this looks nice.
>
> Making the confirm_action implementation access the relevant
> config from the ID may still be a big tricky though (wt, branch
> ?).
>
> This may be addressed by setting some config attribute to the ui
> though... unless we need the context manager for that...

Interesting point. istm that you might want per-context configuration
to affect the ui in more than just confirmations...

So is the overall vote here 'tweak, plus check with gui maintainers,
plus update the other callers'?

--
Martin

Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (4.0 KiB)

>>>>> Martin Pool <email address hidden> writes:

    > On 10 September 2010 02:19, Vincent Ladeuil <email address hidden> wrote:
    >> 117     + def confirm_action(self, prompt, confirmation_id, args, default=True):
    >>
    >> s/args/kwargs/
    >>
    >> or even **kwargs like the others, there should be a way to make
    >> this compatible with 'default=True' (which isn't documented nor
    >> used by the way).

    > Thanks for the speedy review.

    > At the moment I really dislike using **kwargs as shorthand for a dict
    > to be passed to lower level code, eg to be passed in to string
    > interpolating. It makes things slightly easier on the caller it's
    > true, but it also tends to break by clashing with other arguments used
    > either at this level or in different implementations or as the code
    > changes later on.

I see. Well, turning the callerrs x=2, y=3 into dict(x=2, y=3) won't
such a problem. But in this case, the migration from one to the other
should occur sooner rather than later, keeping different idioms in the
code base... is painful in the long run.

    >> Oh wait, yes it *is* used in other implementations, ok.
    >>
    >> Hmm, I think that 'confirm' already implies that the default is
    >> True so this may be useless. Do you have a use case for
    >> default=False ?

    > That would imply an operation where we want confirmation from a user
    > and we won't proceed in non-interactive mode, which is conceivable but
    > unlikely. So perhaps I should just delete it.

Then we could introduce deny_action when needed ?

    >>
    >> Adding confirm_action to uis sounds like a good idea, but this
    >> needs to be discussed with the qbzr/bzr-gtk people since they
    >> need a way to override it without calling get_boolean for which
    >> they probably also defines aspecific version...
    >>
    >> This may not be a bzrlib problem though...

    > I'll cc them and they may have a thought.

    >> As mentionned on IRC, I like the direction this is taking
    >> regarding make_test_ui_factory being a good step towards a
    >> fixture independent from the test itself.
    >>
    >> Regarding 'bzr.lock.break.confirm', I'm fine with using an
    >> arbitrary name space but sticking to the python module name space
    >> may be better suited in the long term (easier to discover and to maintain).

    > So bzrlib.lockdir.break, or something similar?

At least internally, but the caller itself may use an aliased form.

    >> Now, reagrding the relationships with config variables, we
    >> can (hypothetically):
    >>
    >> - define an alias between bzr and bzrlib or
    >>  bzr.lock.break.confirm and bzrlib.lock.confirm_break or,
    >>  even simpler, between bzrlib.lock.confirm_break and confirm_break_lock
    >>
    >> - define (handwaving) a config var as bzrlib.lock.confirm_break
    >>  and import it in bzrlib.lockdir
    >>
    >> Or any combination.
    >>
    >> Anyway, I now better understand what you mean for the application
    >> code and indeed this looks nice.
    >>
    >> Making the confirm_action implementation access the relevant
    >> config from the ID may still be a big...

Read more...

Revision history for this message
Vincent Ladeuil (vila) wrote :

116 + def confirm_action(self, prompt, confirmation_id, prompt_kwargs):

Grr, sorry to notice this so late, but isn't:
  confirm_action(self, confirmation_id, prompt,prompt_kwargs):

more explicit (your docstring and dev doc focuses on the confirmation_id
which is also a good hint) ?
I.e. the confirmation id, followed by the prompt and its args ?

And regarding check with gui maintainers, yes.

Revision history for this message
Martin Pool (mbp) wrote :

> > At the moment I really dislike using **kwargs as shorthand for a dict
> > to be passed to lower level code, eg to be passed in to string
> > interpolating. It makes things slightly easier on the caller it's
> > true, but it also tends to break by clashing with other arguments used
> > either at this level or in different implementations or as the code
> > changes later on.
>
> I see. Well, turning the callerrs x=2, y=3 into dict(x=2, y=3) won't
> such a problem. But in this case, the migration from one to the other
> should occur sooner rather than later, keeping different idioms in the
> code base... is painful in the long run.

If you're talking about going back to clean up all existing cases like this, then it is a bit painful. I think not adding any more of them is worthwhile.

I think the coding guideline would be, don't mix kwargs where the caller could want provide any arbitrary value with other parameters. Use kwargs only for metaprogramming. Perhaps this is too harsh and needlessly closes off a useful tool though.

> >> Oh wait, yes it *is* used in other implementations, ok.
> >>
> >> Hmm, I think that 'confirm' already implies that the default is
> >> True so this may be useless. Do you have a use case for
> >> default=False ?
>
> > That would imply an operation where we want confirmation from a user
> > and we won't proceed in non-interactive mode, which is conceivable but
> > unlikely. So perhaps I should just delete it.
>
> Then we could introduce deny_action when needed ?

If we think we'll want it, perhaps we should keep the option now?

> > So bzrlib.lockdir.break, or something similar?
>
> At least internally, but the caller itself may use an aliased form.

I think having aliases for this might just cause confusion. My updated branch shows some suggested names.

Revision history for this message
Gary van der Merwe (garyvdm) wrote :

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

On 09/09/2010 18:19, Vincent Ladeuil wrote:
> Adding confirm_action to uis sounds like a good idea, but this
> needs to be discussed with the qbzr/bzr-gtk people since they
> need a way to override it without calling get_boolean for which
> they probably also defines aspecific version...

The patch seems cool to me. I can override both got_boolean, and
confirm_action. It would be be cool if confirm_action showed a dialog
with a "Don't ask me this again." checkbox.

review:approve
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkyJ3n4ACgkQd/3EdwGKOh0IWACeKcVRS78gXrqz8Akd0SH/U+zz
NrEAnivrpxukezT+t906ayEYiq3ZGKGF
=MM58
-----END PGP SIGNATURE-----

Revision history for this message
Vincent Ladeuil (vila) wrote :

>>>>> Martin Pool <email address hidden> writes:

    >> > At the moment I really dislike using **kwargs as shorthand for a dict
    >> > to be passed to lower level code, eg to be passed in to string
    >> > interpolating. It makes things slightly easier on the caller it's
    >> > true, but it also tends to break by clashing with other arguments used
    >> > either at this level or in different implementations or as the code
    >> > changes later on.
    >>
    >> I see. Well, turning the callerrs x=2, y=3 into dict(x=2, y=3) won't
    >> such a problem. But in this case, the migration from one to the other
    >> should occur sooner rather than later, keeping different idioms in the
    >> code base... is painful in the long run.

    > If you're talking about going back to clean up all existing cases
    > like this, then it is a bit painful. I think not adding any more
    > of them is worthwhile.

It will be painful *once*, but less painful for new contributors that
won't wonder which one to use, for reviewers who won't have to say "you
picked the wrong one", etc. Just saying ;)

<snip/>

    >> > That would imply an operation where we want confirmation from a user
    >> > and we won't proceed in non-interactive mode, which is conceivable but
    >> > unlikely. So perhaps I should just delete it.
    >>
    >> Then we could introduce deny_action when needed ?

    > If we think we'll want it, perhaps we should keep the option now?

confirm_action(default=False) sounds really akward compared to
deny_action()...

    >> > So bzrlib.lockdir.break, or something similar?
    >>
    >> At least internally, but the caller itself may use an aliased form.

    > I think having aliases for this might just cause confusion. My
    > updated branch shows some suggested names.

Could be, let's see how it's used (but we already have many conf vars
that could benefit to be associated with a python name and then we'll
have to ensure backward compatibility anyway).

Revision history for this message
Martin Pool (mbp) wrote :

On 10 September 2010 18:43, Vincent Ladeuil <email address hidden> wrote:
>>>>>> Martin Pool <email address hidden> writes:
>
>    >> > At the moment I really dislike using **kwargs as shorthand for a dict
>    >> > to be passed to lower level code, eg to be passed in to string
>    >> > interpolating.  It makes things slightly easier on the caller it's
>    >> > true, but it also tends to break by clashing with other arguments used
>    >> > either at this level or in different implementations or as the code
>    >> > changes later on.
>    >>
>    >> I see. Well, turning the callerrs x=2, y=3 into dict(x=2, y=3) won't
>    >> such a problem. But in this case, the migration from one to the other
>    >> should occur sooner rather than later, keeping different idioms in the
>    >> code base... is painful in the long run.
>
>    > If you're talking about going back to clean up all existing cases
>    > like this, then it is a bit painful.  I think not adding any more
>    > of them is worthwhile.
>
> It will be painful *once*, but less painful for new contributors that
> won't wonder which one to use, for reviewers who won't have to say "you
> picked the wrong one", etc. Just saying ;)

Let's see if we can work out a clear expression of the code guideline.
 I don't want to ban kwargs altogether, but I think it's bad to
promise people can pass arbitrary dicts through to a lower level (eg a
format string), when the function also takes other arguments. You
should instead pass a dict.

--
Martin

Revision history for this message
Andrew Bennetts (spiv) wrote :

Regarding this patch: I'm in favour of it. There's a nit that default=True still exists in the signature of one of the implementations, so Needs Fixing.

I'm not worried about the slight inconvenience of an explicit dict rather than **kwargs. There just aren't enough places where we ask users to confirm an action for this to be a big issue. And I do like the explicit separation of function params vs. string interpolation params.

Regarding the general guidelines for kwargs: I think it should be used sparingly. I think it can be good when a function is delegating largely to another callable (e.g. it is some sort of higher order function, like TestCase.addCleanup). Otherwise I tend to be sceptical. If the parameters to a function are a bit complex and might vary over time (e.g. the commit API) then I think what's really called for is an explicit object for the parameters (e.g. I think we should create a CommitParams object), rather than a bag of positional and/or keyword args. If you have an arbitrary set of keys and values that are different with each use (e.g. string interpolation inputs) then again I think that should not be mixed in with the regular positional/keyword args, it seems like a different category of thing.

I guess a short rule-of-thumb is: be wary of any **kwargs that aren't accompanied by a param called 'callable'.

review: Needs Fixing
Revision history for this message
Martin Pool (mbp) wrote :
Revision history for this message
Martin Pool (mbp) wrote :

This now fixes up all the immediately obvious callers of get_boolean.

We could do more as far as configuration etc but I think it's good to land.

Revision history for this message
Vincent Ladeuil (vila) wrote :

Great !

review: Approve
Revision history for this message
Martin Pool (mbp) wrote :

sent to pqm by email

Revision history for this message
Martin Pool (mbp) wrote :

sent to pqm by email

Revision history for this message
Martin Pool (mbp) wrote :

sent to pqm by email

Revision history for this message
Martin Pool (mbp) wrote :

sent to pqm by email

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 2010-09-14 09:29:57 +0000
3+++ bzrlib/builtins.py 2010-09-15 10:43:44 +0000
4@@ -4801,8 +4801,11 @@
5 self.outf.write('The above revision(s) will be removed.\n')
6
7 if not force:
8- if not ui.ui_factory.get_boolean('Are you sure'):
9- self.outf.write('Canceled')
10+ if not ui.ui_factory.confirm_action(
11+ 'Uncommit these revisions',
12+ 'bzrlib.builtins.uncommit',
13+ {}):
14+ self.outf.write('Canceled\n')
15 return 0
16
17 mutter('Uncommitting from {%s} to {%s}',
18
19=== modified file 'bzrlib/lockdir.py'
20--- bzrlib/lockdir.py 2010-09-13 02:32:44 +0000
21+++ bzrlib/lockdir.py 2010-09-15 10:43:44 +0000
22@@ -358,7 +358,9 @@
23 return
24 if holder_info is not None:
25 lock_info = '\n'.join(self._format_lock_info(holder_info))
26- if bzrlib.ui.ui_factory.get_boolean("Break %s" % lock_info):
27+ if bzrlib.ui.ui_factory.confirm_action(
28+ "Break %(lock_info)s", 'bzrlib.lockdir.break',
29+ dict(lock_info=lock_info)):
30 self.force_break(holder_info)
31
32 def force_break(self, dead_holder_info):
33
34=== modified file 'bzrlib/msgeditor.py'
35--- bzrlib/msgeditor.py 2010-04-30 11:35:43 +0000
36+++ bzrlib/msgeditor.py 2010-09-15 10:43:44 +0000
37@@ -149,8 +149,10 @@
38 return None
39 edited_content = msg_transport.get_bytes(basename)
40 if edited_content == reference_content:
41- if not ui.ui_factory.get_boolean(
42- "Commit message was not edited, use anyway"):
43+ if not ui.ui_factory.confirm_action(
44+ "Commit message was not edited, use anyway",
45+ "bzrlib.msgeditor.unchanged",
46+ {}):
47 # Returning "" makes cmd_commit raise 'empty commit message
48 # specified' which is a reasonable error, given the user has
49 # rejected using the unedited template.
50
51=== modified file 'bzrlib/tests/blackbox/test_uncommit.py'
52--- bzrlib/tests/blackbox/test_uncommit.py 2010-03-01 16:18:43 +0000
53+++ bzrlib/tests/blackbox/test_uncommit.py 2010-09-15 10:43:44 +0000
54@@ -22,7 +22,10 @@
55 from bzrlib.bzrdir import BzrDirMetaFormat1
56 from bzrlib.errors import BzrError, BoundBranchOutOfDate
57 from bzrlib.tests import TestCaseWithTransport
58-from bzrlib.tests.script import ScriptRunner
59+from bzrlib.tests.script import (
60+ run_script,
61+ ScriptRunner,
62+ )
63
64
65 class TestUncommit(TestCaseWithTransport):
66@@ -61,6 +64,20 @@
67 out, err = self.run_bzr('status')
68 self.assertEquals(out, 'modified:\n a\n')
69
70+ def test_uncommit_interactive(self):
71+ """Uncommit seeks confirmation, and doesn't proceed without it."""
72+ wt = self.create_simple_tree()
73+ os.chdir('tree')
74+ run_script(self, """
75+ $ bzr uncommit
76+ ...
77+ The above revision(s) will be removed.
78+ 2>Uncommit these revisions? [y/n]:
79+ <n
80+ Canceled
81+ """)
82+ self.assertEqual(['a2'], wt.get_parent_ids())
83+
84 def test_uncommit_no_history(self):
85 wt = self.make_branch_and_tree('tree')
86 out, err = self.run_bzr('uncommit --force', retcode=1)
87
88=== modified file 'bzrlib/tests/per_uifactory/__init__.py'
89--- bzrlib/tests/per_uifactory/__init__.py 2010-02-23 07:43:11 +0000
90+++ bzrlib/tests/per_uifactory/__init__.py 2010-09-15 10:43:44 +0000
91@@ -62,6 +62,18 @@
92 self.factory.be_quiet(False)
93 self.assertEquals(False, self.factory.is_quiet())
94
95+ def test_confirm_action(self):
96+ # confirm_action should be answered by every ui factory; even
97+ # noninteractive ones should have a reasonable default
98+ self._load_responses([True])
99+ result = self.factory.confirm_action(
100+ 'Break a lock?',
101+ 'bzr.lock.break.confirm',
102+ {})
103+ # will be true either because we read it from the input or because
104+ # that's the default
105+ self.assertEquals(result, True)
106+
107 def test_note(self):
108 self.factory.note("a note to the user")
109 self._check_note("a note to the user")
110@@ -150,6 +162,11 @@
111 # Without a TTY, we shouldn't display anything
112 self.assertEqual('', self.stderr.getvalue())
113
114+ def _load_responses(self, responses):
115+ self.factory.stdin.seek(0)
116+ self.factory.stdin.writelines([(r and "y\n" or "n\n") for r in responses])
117+ self.factory.stdin.seek(0)
118+
119
120 class TestTTYTextUIFactory(TestTextUIFactory):
121
122@@ -222,6 +239,9 @@
123 def _check_log_transport_activity_display_no_bytes(self):
124 pass
125
126+ def _load_responses(self, responses):
127+ pass
128+
129
130 class TestCannedInputUIFactory(tests.TestCase, UIFactoryTestMixin):
131 # discards output, reads input from variables
132@@ -250,3 +270,6 @@
133
134 def _check_log_transport_activity_display_no_bytes(self):
135 pass
136+
137+ def _load_responses(self, responses):
138+ self.factory.responses.extend(responses)
139
140=== modified file 'bzrlib/tests/test_ui.py'
141--- bzrlib/tests/test_ui.py 2010-09-14 08:14:22 +0000
142+++ bzrlib/tests/test_ui.py 2010-09-15 10:43:44 +0000
143@@ -18,11 +18,12 @@
144 """
145
146 import os
147-import re
148 import time
149
150 from StringIO import StringIO
151
152+from testtools.matchers import *
153+
154 from bzrlib import (
155 config,
156 errors,
157@@ -53,16 +54,28 @@
158 ui = tests.TestUIFactory(stdin=None,
159 stdout=tests.StringIOWrapper(),
160 stderr=tests.StringIOWrapper())
161- os = ui.make_output_stream()
162- self.assertEquals(os.encoding, enc)
163+ output = ui.make_output_stream()
164+ self.assertEquals(output.encoding, enc)
165
166
167 class TestTextUIFactory(tests.TestCase):
168
169+ def make_test_ui_factory(self, stdin_contents):
170+ ui = tests.TestUIFactory(stdin=stdin_contents,
171+ stdout=tests.StringIOWrapper(),
172+ stderr=tests.StringIOWrapper())
173+ return ui
174+
175+ def test_text_factory_confirm(self):
176+ # turns into reading a regular boolean
177+ ui = self.make_test_ui_factory('n\n')
178+ self.assertEquals(ui.confirm_action('Should %(thing)s pass?',
179+ 'bzrlib.tests.test_ui.confirmation',
180+ {'thing': 'this'},),
181+ False)
182+
183 def test_text_factory_ascii_password(self):
184- ui = tests.TestUIFactory(stdin='secret\n',
185- stdout=tests.StringIOWrapper(),
186- stderr=tests.StringIOWrapper())
187+ ui = self.make_test_ui_factory('secret\n')
188 pb = ui.nested_progress_bar()
189 try:
190 self.assertEqual('secret',
191@@ -83,9 +96,7 @@
192 We can't predict what encoding users will have for stdin, so we force
193 it to utf8 to test that we transport the password correctly.
194 """
195- ui = tests.TestUIFactory(stdin=u'baz\u1234'.encode('utf8'),
196- stdout=tests.StringIOWrapper(),
197- stderr=tests.StringIOWrapper())
198+ ui = self.make_test_ui_factory(u'baz\u1234'.encode('utf8'))
199 ui.stderr.encoding = ui.stdout.encoding = ui.stdin.encoding = 'utf8'
200 pb = ui.nested_progress_bar()
201 try:
202@@ -437,6 +448,39 @@
203 self.assertIsNone('off', av)
204
205
206+class TestConfirmationUserInterfacePolicy(tests.TestCase):
207+
208+ def test_confirm_action_default(self):
209+ base_ui = _mod_ui.NoninteractiveUIFactory()
210+ for answer in [True, False]:
211+ self.assertEquals(
212+ _mod_ui.ConfirmationUserInterfacePolicy(base_ui, answer, {})
213+ .confirm_action("Do something?",
214+ "bzrlib.tests.do_something", {}),
215+ answer)
216+
217+ def test_confirm_action_specific(self):
218+ base_ui = _mod_ui.NoninteractiveUIFactory()
219+ for default_answer in [True, False]:
220+ for specific_answer in [True, False]:
221+ for conf_id in ['given_id', 'other_id']:
222+ wrapper = _mod_ui.ConfirmationUserInterfacePolicy(
223+ base_ui, default_answer, dict(given_id=specific_answer))
224+ result = wrapper.confirm_action("Do something?", conf_id, {})
225+ if conf_id == 'given_id':
226+ self.assertEquals(result, specific_answer)
227+ else:
228+ self.assertEquals(result, default_answer)
229+
230+ def test_repr(self):
231+ base_ui = _mod_ui.NoninteractiveUIFactory()
232+ wrapper = _mod_ui.ConfirmationUserInterfacePolicy(
233+ base_ui, True, dict(a=2))
234+ self.assertThat(repr(wrapper),
235+ Equals("ConfirmationUserInterfacePolicy("
236+ "NoninteractiveUIFactory(), True, {'a': 2})"))
237+
238+
239 class TestProgressRecordingUI(tests.TestCase):
240 """Test test-oriented UIFactory that records progress updates"""
241
242
243=== modified file 'bzrlib/ui/__init__.py'
244--- bzrlib/ui/__init__.py 2010-06-25 20:34:05 +0000
245+++ bzrlib/ui/__init__.py 2010-09-15 10:43:44 +0000
246@@ -100,6 +100,42 @@
247 return val
248
249
250+class ConfirmationUserInterfacePolicy(object):
251+ """Wrapper for a UIFactory that allows or denies all confirmed actions."""
252+
253+ def __init__(self, wrapped_ui, default_answer, specific_answers):
254+ """Generate a proxy UI that does no confirmations.
255+
256+ :param wrapped_ui: Underlying UIFactory.
257+ :param default_answer: Bool for whether requests for
258+ confirmation from the user should be noninteractively accepted or
259+ denied.
260+ :param specific_answers: Map from confirmation_id to bool answer.
261+ """
262+ self.wrapped_ui = wrapped_ui
263+ self.default_answer = default_answer
264+ self.specific_answers = specific_answers
265+
266+ def __getattr__(self, name):
267+ return getattr(self.wrapped_ui, name)
268+
269+ def __repr__(self):
270+ return '%s(%r, %r, %r)' % (
271+ self.__class__.__name__,
272+ self.wrapped_ui,
273+ self.default_answer,
274+ self.specific_answers)
275+
276+ def confirm_action(self, prompt, confirmation_id, prompt_kwargs):
277+ if confirmation_id in self.specific_answers:
278+ return self.specific_answers[confirmation_id]
279+ elif self.default_answer is not None:
280+ return self.default_answer
281+ else:
282+ return self.wrapped_ui.confirm_action(
283+ prompt, confirmation_id, prompt_kwargs)
284+
285+
286 class UIFactory(object):
287 """UI abstraction.
288
289@@ -151,6 +187,24 @@
290 """
291 self._quiet = state
292
293+ def confirm_action(self, prompt, confirmation_id, prompt_kwargs):
294+ """Seek user confirmation for an action.
295+
296+ If the UI is noninteractive, or the user does not want to be asked
297+ about this action, True is returned, indicating bzr should just
298+ proceed.
299+
300+ The confirmation id allows the user to configure certain actions to
301+ always be confirmed or always denied, and for UIs to specialize the
302+ display of particular confirmations.
303+
304+ :param prompt: Suggested text to display to the user.
305+ :param prompt_kwargs: A dictionary of arguments that can be
306+ string-interpolated into the prompt.
307+ :param confirmation_id: Unique string identifier for the confirmation.
308+ """
309+ return self.get_boolean(prompt % prompt_kwargs)
310+
311 def get_password(self, prompt='', **kwargs):
312 """Prompt the user for a password.
313
314@@ -377,7 +431,17 @@
315 "without an upgrade path.\n" % (inter.target._format,))
316
317
318-class SilentUIFactory(UIFactory):
319+class NoninteractiveUIFactory(UIFactory):
320+ """Base class for UIs with no user."""
321+
322+ def confirm_action(self, prompt, confirmation_id, prompt_kwargs):
323+ return True
324+
325+ def __repr__(self):
326+ return '%s()' % (self.__class__.__name__, )
327+
328+
329+class SilentUIFactory(NoninteractiveUIFactory):
330 """A UI Factory which never prints anything.
331
332 This is the default UI, if another one is never registered by a program
333@@ -418,6 +482,9 @@
334 def __repr__(self):
335 return "%s(%r)" % (self.__class__.__name__, self.responses)
336
337+ def confirm_action(self, prompt, confirmation_id, args):
338+ return self.get_boolean(prompt % args)
339+
340 def get_boolean(self, prompt):
341 return self.responses.pop(0)
342
343
344=== modified file 'doc/developers/ui.txt'
345--- doc/developers/ui.txt 2010-09-03 09:14:12 +0000
346+++ doc/developers/ui.txt 2010-09-15 10:43:44 +0000
347@@ -1,6 +1,6 @@
348-==========================================
349-Bazaar Developer Guide to User Interaction
350-==========================================
351+=========================
352+Interacting with the user
353+=========================
354
355 Getting Input
356 =============
357@@ -24,6 +24,40 @@
358 used for programmer convenience, but not performing unpredictably in the
359 presence of different locales.
360
361+Confirmation
362+============
363+
364+There are some operations, such as uncommitting, or breaking a lock, where
365+bzr may want to get confirmation from the user before proceeding.
366+However in some circumstances bzr should just go ahead without asking: if
367+it's being used from a noninteractive program, or if the user's asked to
368+never be asked for this particular confirmation or for any confirmations
369+at all.
370+
371+We provide a special UIFactory method `confirm_action` to do this. It
372+takes a `confirmation_id` parameter that acts as a symbolic name for the
373+type of confirmation, so the user can configure them off. (This is not
374+implemented at present.) GUIs can have a "don't ask me again" option
375+keyed by the confirmation id.
376+
377+Confirmation ids look like Python paths to the logical code that should
378+use them. (Because code may move or the check may for implementation
379+reasons be done elsewhere, they need not perfectly correspond to the place
380+they're used, and they should stay stable even when the code moves.)
381+
382+``bzrlib.builtins.uncommit``
383+ Before the ``uncommit`` command actually changes the branch.
384+
385+``bzrlib.lockdir.break``
386+ Before breaking a lock.
387+
388+``bzrlib.msgeditor.unchanged``
389+ Proceed even though the user made no changes to the template message.
390+
391+Interactive confirmations can be overridden by using a
392+`ConfirmationUserInterfacePolicy` decorator as the default
393+ui_factory.
394+
395
396 Writing Output
397 ==============