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
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2010-09-14 09:29:57 +0000
+++ bzrlib/builtins.py 2010-09-15 10:43:44 +0000
@@ -4801,8 +4801,11 @@
4801 self.outf.write('The above revision(s) will be removed.\n')4801 self.outf.write('The above revision(s) will be removed.\n')
48024802
4803 if not force:4803 if not force:
4804 if not ui.ui_factory.get_boolean('Are you sure'):4804 if not ui.ui_factory.confirm_action(
4805 self.outf.write('Canceled')4805 'Uncommit these revisions',
4806 'bzrlib.builtins.uncommit',
4807 {}):
4808 self.outf.write('Canceled\n')
4806 return 04809 return 0
48074810
4808 mutter('Uncommitting from {%s} to {%s}',4811 mutter('Uncommitting from {%s} to {%s}',
48094812
=== modified file 'bzrlib/lockdir.py'
--- bzrlib/lockdir.py 2010-09-13 02:32:44 +0000
+++ bzrlib/lockdir.py 2010-09-15 10:43:44 +0000
@@ -358,7 +358,9 @@
358 return358 return
359 if holder_info is not None:359 if holder_info is not None:
360 lock_info = '\n'.join(self._format_lock_info(holder_info))360 lock_info = '\n'.join(self._format_lock_info(holder_info))
361 if bzrlib.ui.ui_factory.get_boolean("Break %s" % lock_info):361 if bzrlib.ui.ui_factory.confirm_action(
362 "Break %(lock_info)s", 'bzrlib.lockdir.break',
363 dict(lock_info=lock_info)):
362 self.force_break(holder_info)364 self.force_break(holder_info)
363365
364 def force_break(self, dead_holder_info):366 def force_break(self, dead_holder_info):
365367
=== modified file 'bzrlib/msgeditor.py'
--- bzrlib/msgeditor.py 2010-04-30 11:35:43 +0000
+++ bzrlib/msgeditor.py 2010-09-15 10:43:44 +0000
@@ -149,8 +149,10 @@
149 return None149 return None
150 edited_content = msg_transport.get_bytes(basename)150 edited_content = msg_transport.get_bytes(basename)
151 if edited_content == reference_content:151 if edited_content == reference_content:
152 if not ui.ui_factory.get_boolean(152 if not ui.ui_factory.confirm_action(
153 "Commit message was not edited, use anyway"):153 "Commit message was not edited, use anyway",
154 "bzrlib.msgeditor.unchanged",
155 {}):
154 # Returning "" makes cmd_commit raise 'empty commit message156 # Returning "" makes cmd_commit raise 'empty commit message
155 # specified' which is a reasonable error, given the user has157 # specified' which is a reasonable error, given the user has
156 # rejected using the unedited template.158 # rejected using the unedited template.
157159
=== modified file 'bzrlib/tests/blackbox/test_uncommit.py'
--- bzrlib/tests/blackbox/test_uncommit.py 2010-03-01 16:18:43 +0000
+++ bzrlib/tests/blackbox/test_uncommit.py 2010-09-15 10:43:44 +0000
@@ -22,7 +22,10 @@
22from bzrlib.bzrdir import BzrDirMetaFormat122from bzrlib.bzrdir import BzrDirMetaFormat1
23from bzrlib.errors import BzrError, BoundBranchOutOfDate23from bzrlib.errors import BzrError, BoundBranchOutOfDate
24from bzrlib.tests import TestCaseWithTransport24from bzrlib.tests import TestCaseWithTransport
25from bzrlib.tests.script import ScriptRunner25from bzrlib.tests.script import (
26 run_script,
27 ScriptRunner,
28 )
2629
2730
28class TestUncommit(TestCaseWithTransport):31class TestUncommit(TestCaseWithTransport):
@@ -61,6 +64,20 @@
61 out, err = self.run_bzr('status')64 out, err = self.run_bzr('status')
62 self.assertEquals(out, 'modified:\n a\n')65 self.assertEquals(out, 'modified:\n a\n')
6366
67 def test_uncommit_interactive(self):
68 """Uncommit seeks confirmation, and doesn't proceed without it."""
69 wt = self.create_simple_tree()
70 os.chdir('tree')
71 run_script(self, """
72 $ bzr uncommit
73 ...
74 The above revision(s) will be removed.
75 2>Uncommit these revisions? [y/n]:
76 <n
77 Canceled
78 """)
79 self.assertEqual(['a2'], wt.get_parent_ids())
80
64 def test_uncommit_no_history(self):81 def test_uncommit_no_history(self):
65 wt = self.make_branch_and_tree('tree')82 wt = self.make_branch_and_tree('tree')
66 out, err = self.run_bzr('uncommit --force', retcode=1)83 out, err = self.run_bzr('uncommit --force', retcode=1)
6784
=== modified file 'bzrlib/tests/per_uifactory/__init__.py'
--- bzrlib/tests/per_uifactory/__init__.py 2010-02-23 07:43:11 +0000
+++ bzrlib/tests/per_uifactory/__init__.py 2010-09-15 10:43:44 +0000
@@ -62,6 +62,18 @@
62 self.factory.be_quiet(False)62 self.factory.be_quiet(False)
63 self.assertEquals(False, self.factory.is_quiet())63 self.assertEquals(False, self.factory.is_quiet())
6464
65 def test_confirm_action(self):
66 # confirm_action should be answered by every ui factory; even
67 # noninteractive ones should have a reasonable default
68 self._load_responses([True])
69 result = self.factory.confirm_action(
70 'Break a lock?',
71 'bzr.lock.break.confirm',
72 {})
73 # will be true either because we read it from the input or because
74 # that's the default
75 self.assertEquals(result, True)
76
65 def test_note(self):77 def test_note(self):
66 self.factory.note("a note to the user")78 self.factory.note("a note to the user")
67 self._check_note("a note to the user")79 self._check_note("a note to the user")
@@ -150,6 +162,11 @@
150 # Without a TTY, we shouldn't display anything162 # Without a TTY, we shouldn't display anything
151 self.assertEqual('', self.stderr.getvalue())163 self.assertEqual('', self.stderr.getvalue())
152164
165 def _load_responses(self, responses):
166 self.factory.stdin.seek(0)
167 self.factory.stdin.writelines([(r and "y\n" or "n\n") for r in responses])
168 self.factory.stdin.seek(0)
169
153170
154class TestTTYTextUIFactory(TestTextUIFactory):171class TestTTYTextUIFactory(TestTextUIFactory):
155172
@@ -222,6 +239,9 @@
222 def _check_log_transport_activity_display_no_bytes(self):239 def _check_log_transport_activity_display_no_bytes(self):
223 pass240 pass
224241
242 def _load_responses(self, responses):
243 pass
244
225245
226class TestCannedInputUIFactory(tests.TestCase, UIFactoryTestMixin):246class TestCannedInputUIFactory(tests.TestCase, UIFactoryTestMixin):
227 # discards output, reads input from variables247 # discards output, reads input from variables
@@ -250,3 +270,6 @@
250270
251 def _check_log_transport_activity_display_no_bytes(self):271 def _check_log_transport_activity_display_no_bytes(self):
252 pass272 pass
273
274 def _load_responses(self, responses):
275 self.factory.responses.extend(responses)
253276
=== modified file 'bzrlib/tests/test_ui.py'
--- bzrlib/tests/test_ui.py 2010-09-14 08:14:22 +0000
+++ bzrlib/tests/test_ui.py 2010-09-15 10:43:44 +0000
@@ -18,11 +18,12 @@
18"""18"""
1919
20import os20import os
21import re
22import time21import time
2322
24from StringIO import StringIO23from StringIO import StringIO
2524
25from testtools.matchers import *
26
26from bzrlib import (27from bzrlib import (
27 config,28 config,
28 errors,29 errors,
@@ -53,16 +54,28 @@
53 ui = tests.TestUIFactory(stdin=None,54 ui = tests.TestUIFactory(stdin=None,
54 stdout=tests.StringIOWrapper(),55 stdout=tests.StringIOWrapper(),
55 stderr=tests.StringIOWrapper())56 stderr=tests.StringIOWrapper())
56 os = ui.make_output_stream()57 output = ui.make_output_stream()
57 self.assertEquals(os.encoding, enc)58 self.assertEquals(output.encoding, enc)
5859
5960
60class TestTextUIFactory(tests.TestCase):61class TestTextUIFactory(tests.TestCase):
6162
63 def make_test_ui_factory(self, stdin_contents):
64 ui = tests.TestUIFactory(stdin=stdin_contents,
65 stdout=tests.StringIOWrapper(),
66 stderr=tests.StringIOWrapper())
67 return ui
68
69 def test_text_factory_confirm(self):
70 # turns into reading a regular boolean
71 ui = self.make_test_ui_factory('n\n')
72 self.assertEquals(ui.confirm_action('Should %(thing)s pass?',
73 'bzrlib.tests.test_ui.confirmation',
74 {'thing': 'this'},),
75 False)
76
62 def test_text_factory_ascii_password(self):77 def test_text_factory_ascii_password(self):
63 ui = tests.TestUIFactory(stdin='secret\n',78 ui = self.make_test_ui_factory('secret\n')
64 stdout=tests.StringIOWrapper(),
65 stderr=tests.StringIOWrapper())
66 pb = ui.nested_progress_bar()79 pb = ui.nested_progress_bar()
67 try:80 try:
68 self.assertEqual('secret',81 self.assertEqual('secret',
@@ -83,9 +96,7 @@
83 We can't predict what encoding users will have for stdin, so we force96 We can't predict what encoding users will have for stdin, so we force
84 it to utf8 to test that we transport the password correctly.97 it to utf8 to test that we transport the password correctly.
85 """98 """
86 ui = tests.TestUIFactory(stdin=u'baz\u1234'.encode('utf8'),99 ui = self.make_test_ui_factory(u'baz\u1234'.encode('utf8'))
87 stdout=tests.StringIOWrapper(),
88 stderr=tests.StringIOWrapper())
89 ui.stderr.encoding = ui.stdout.encoding = ui.stdin.encoding = 'utf8'100 ui.stderr.encoding = ui.stdout.encoding = ui.stdin.encoding = 'utf8'
90 pb = ui.nested_progress_bar()101 pb = ui.nested_progress_bar()
91 try:102 try:
@@ -437,6 +448,39 @@
437 self.assertIsNone('off', av)448 self.assertIsNone('off', av)
438449
439450
451class TestConfirmationUserInterfacePolicy(tests.TestCase):
452
453 def test_confirm_action_default(self):
454 base_ui = _mod_ui.NoninteractiveUIFactory()
455 for answer in [True, False]:
456 self.assertEquals(
457 _mod_ui.ConfirmationUserInterfacePolicy(base_ui, answer, {})
458 .confirm_action("Do something?",
459 "bzrlib.tests.do_something", {}),
460 answer)
461
462 def test_confirm_action_specific(self):
463 base_ui = _mod_ui.NoninteractiveUIFactory()
464 for default_answer in [True, False]:
465 for specific_answer in [True, False]:
466 for conf_id in ['given_id', 'other_id']:
467 wrapper = _mod_ui.ConfirmationUserInterfacePolicy(
468 base_ui, default_answer, dict(given_id=specific_answer))
469 result = wrapper.confirm_action("Do something?", conf_id, {})
470 if conf_id == 'given_id':
471 self.assertEquals(result, specific_answer)
472 else:
473 self.assertEquals(result, default_answer)
474
475 def test_repr(self):
476 base_ui = _mod_ui.NoninteractiveUIFactory()
477 wrapper = _mod_ui.ConfirmationUserInterfacePolicy(
478 base_ui, True, dict(a=2))
479 self.assertThat(repr(wrapper),
480 Equals("ConfirmationUserInterfacePolicy("
481 "NoninteractiveUIFactory(), True, {'a': 2})"))
482
483
440class TestProgressRecordingUI(tests.TestCase):484class TestProgressRecordingUI(tests.TestCase):
441 """Test test-oriented UIFactory that records progress updates"""485 """Test test-oriented UIFactory that records progress updates"""
442486
443487
=== modified file 'bzrlib/ui/__init__.py'
--- bzrlib/ui/__init__.py 2010-06-25 20:34:05 +0000
+++ bzrlib/ui/__init__.py 2010-09-15 10:43:44 +0000
@@ -100,6 +100,42 @@
100 return val100 return val
101101
102102
103class ConfirmationUserInterfacePolicy(object):
104 """Wrapper for a UIFactory that allows or denies all confirmed actions."""
105
106 def __init__(self, wrapped_ui, default_answer, specific_answers):
107 """Generate a proxy UI that does no confirmations.
108
109 :param wrapped_ui: Underlying UIFactory.
110 :param default_answer: Bool for whether requests for
111 confirmation from the user should be noninteractively accepted or
112 denied.
113 :param specific_answers: Map from confirmation_id to bool answer.
114 """
115 self.wrapped_ui = wrapped_ui
116 self.default_answer = default_answer
117 self.specific_answers = specific_answers
118
119 def __getattr__(self, name):
120 return getattr(self.wrapped_ui, name)
121
122 def __repr__(self):
123 return '%s(%r, %r, %r)' % (
124 self.__class__.__name__,
125 self.wrapped_ui,
126 self.default_answer,
127 self.specific_answers)
128
129 def confirm_action(self, prompt, confirmation_id, prompt_kwargs):
130 if confirmation_id in self.specific_answers:
131 return self.specific_answers[confirmation_id]
132 elif self.default_answer is not None:
133 return self.default_answer
134 else:
135 return self.wrapped_ui.confirm_action(
136 prompt, confirmation_id, prompt_kwargs)
137
138
103class UIFactory(object):139class UIFactory(object):
104 """UI abstraction.140 """UI abstraction.
105141
@@ -151,6 +187,24 @@
151 """187 """
152 self._quiet = state188 self._quiet = state
153189
190 def confirm_action(self, prompt, confirmation_id, prompt_kwargs):
191 """Seek user confirmation for an action.
192
193 If the UI is noninteractive, or the user does not want to be asked
194 about this action, True is returned, indicating bzr should just
195 proceed.
196
197 The confirmation id allows the user to configure certain actions to
198 always be confirmed or always denied, and for UIs to specialize the
199 display of particular confirmations.
200
201 :param prompt: Suggested text to display to the user.
202 :param prompt_kwargs: A dictionary of arguments that can be
203 string-interpolated into the prompt.
204 :param confirmation_id: Unique string identifier for the confirmation.
205 """
206 return self.get_boolean(prompt % prompt_kwargs)
207
154 def get_password(self, prompt='', **kwargs):208 def get_password(self, prompt='', **kwargs):
155 """Prompt the user for a password.209 """Prompt the user for a password.
156210
@@ -377,7 +431,17 @@
377 "without an upgrade path.\n" % (inter.target._format,))431 "without an upgrade path.\n" % (inter.target._format,))
378432
379433
380class SilentUIFactory(UIFactory):434class NoninteractiveUIFactory(UIFactory):
435 """Base class for UIs with no user."""
436
437 def confirm_action(self, prompt, confirmation_id, prompt_kwargs):
438 return True
439
440 def __repr__(self):
441 return '%s()' % (self.__class__.__name__, )
442
443
444class SilentUIFactory(NoninteractiveUIFactory):
381 """A UI Factory which never prints anything.445 """A UI Factory which never prints anything.
382446
383 This is the default UI, if another one is never registered by a program447 This is the default UI, if another one is never registered by a program
@@ -418,6 +482,9 @@
418 def __repr__(self):482 def __repr__(self):
419 return "%s(%r)" % (self.__class__.__name__, self.responses)483 return "%s(%r)" % (self.__class__.__name__, self.responses)
420484
485 def confirm_action(self, prompt, confirmation_id, args):
486 return self.get_boolean(prompt % args)
487
421 def get_boolean(self, prompt):488 def get_boolean(self, prompt):
422 return self.responses.pop(0)489 return self.responses.pop(0)
423490
424491
=== modified file 'doc/developers/ui.txt'
--- doc/developers/ui.txt 2010-09-03 09:14:12 +0000
+++ doc/developers/ui.txt 2010-09-15 10:43:44 +0000
@@ -1,6 +1,6 @@
1==========================================1=========================
2Bazaar Developer Guide to User Interaction2Interacting with the user
3==========================================3=========================
44
5Getting Input5Getting Input
6=============6=============
@@ -24,6 +24,40 @@
24used for programmer convenience, but not performing unpredictably in the24used for programmer convenience, but not performing unpredictably in the
25presence of different locales.25presence of different locales.
2626
27Confirmation
28============
29
30There are some operations, such as uncommitting, or breaking a lock, where
31bzr may want to get confirmation from the user before proceeding.
32However in some circumstances bzr should just go ahead without asking: if
33it's being used from a noninteractive program, or if the user's asked to
34never be asked for this particular confirmation or for any confirmations
35at all.
36
37We provide a special UIFactory method `confirm_action` to do this. It
38takes a `confirmation_id` parameter that acts as a symbolic name for the
39type of confirmation, so the user can configure them off. (This is not
40implemented at present.) GUIs can have a "don't ask me again" option
41keyed by the confirmation id.
42
43Confirmation ids look like Python paths to the logical code that should
44use them. (Because code may move or the check may for implementation
45reasons be done elsewhere, they need not perfectly correspond to the place
46they're used, and they should stay stable even when the code moves.)
47
48``bzrlib.builtins.uncommit``
49 Before the ``uncommit`` command actually changes the branch.
50
51``bzrlib.lockdir.break``
52 Before breaking a lock.
53
54``bzrlib.msgeditor.unchanged``
55 Proceed even though the user made no changes to the template message.
56
57Interactive confirmations can be overridden by using a
58`ConfirmationUserInterfacePolicy` decorator as the default
59ui_factory.
60
2761
28Writing Output62Writing Output
29==============63==============