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