Merge lp:~mbp/bzr/387717-progress-bar-tty into lp:~bzr/bzr/trunk-old

Proposed by Martin Pool
Status: Merged
Approved by: Martin Pool
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~mbp/bzr/387717-progress-bar-tty
Merge into: lp:~bzr/bzr/trunk-old
Diff against target: 1426 lines
To merge this branch: bzr merge lp:~mbp/bzr/387717-progress-bar-tty
Reviewer Review Type Date Requested Status
Martin Pool Approve
Review via email: mp+9012@code.launchpad.net

This proposal supersedes a proposal from 2009-06-22.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

This stops progress bars being sent to stderr when it's not a file.

(That's not quite the point of bug 387717, but it's still useful.)

There are some more changes in here: ProgressTasks now talk directly to their ProgressView rather than to the UIFactory which makes them more easily testable in isolation, and may let us remove some forwarding methods in the UIFactory. (The factory still constructs the task so it can arrange to have notifications sent where ever it wants.)

Some classes whose constructor is deprecated now have comments pointing this out.

LINES, TERM and COLUMNS are set to specific values when running tests to reduce variation.

Revision history for this message
Andrew Bennetts (spiv) wrote : Posted in a previous version of this proposal

> + # TODO: deprecate passing ui_factory

It would be good to actually do this TODO :)

It shouldn't be hard to do.

> 145 + 'TERM': 'dumb',
> 146 + 'LINES': '25',
> 147 + 'COLUMNS': '80',

(Idle thought... I wonder if the cost of constructing that dictionary every test is a measureable cost on the total test suite run, especially as we keep adding things to it?)

Part of me wonders if it would be better to just set these variables on TestTextProgressView rather than in the base case...

> + task = self.make_task(None, view, 'reticulating splines', 5, 20)

:)

> 248 + self.assertEqual(
> 249 + r'[####| ] a:b:c 1/2'
> 250 + , view._render_line())

I think adding a assertRendering method might reduce boilerplate a little, but it's not a big issue given there's only a few test methods. (And it's only moved code anyway, rather than new code.)

review: Approve
Revision history for this message
Matthew Fuller (fullermd) wrote : Posted in a previous version of this proposal

> * Progress bars are now suppressed again when the environment variable
> + ``BZR_PROGRESS_BAR`` is set to ``none``, or when stderr is directed to
> + a terminal.
> + (Martin Pool, #339385, #387717)

They're suppressed when stderr *IS* directed to a terminal? 8-}

--
Matthew Fuller (MF4839) | <email address hidden>
Systems/Network Administrator | http://www.over-yonder.net/~fullermd/
           On the Internet, nobody can hear you scream.

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

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

Martin Pool wrote:
> Martin Pool has proposed merging lp:~mbp/bzr/387717-progress-bar-tty into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
>
> This stops progress bars being sent to stderr when it's not a file.
>
> (That's not quite the point of bug 387717, but it's still useful.)
>
> There are some more changes in here: ProgressTasks now talk directly to their ProgressView rather than to the UIFactory which makes them more easily testable in isolation, and may let us remove some forwarding methods in the UIFactory. (The factory still constructs the task so it can arrange to have notifications sent where ever it wants.)
>
> Some classes whose constructor is deprecated now have comments pointing this out.
>
> LINES, TERM and COLUMNS are set to specific values when running tests to reduce variation.
>

...

@@ -82,7 +91,9 @@
         self.total_cnt = None
         self.current_cnt = None
         self.msg = ''
+ # TODO: deprecate passing ui_factory
         self.ui_factory = ui_factory
+ self.progress_view = progress_view
         self.show_pct = False
         self.show_spinner = True
         self.show_eta = False,

^- Why not just deprecate it?

if ui_factory is not None:
  symbol_versioning.warn(
    'Passing ui_factory' % symbol_versioning.deprecated_in((1, 17, 0)),
    ' Pass progress_view instead')

or are there still bits that use the ui_factory without going to
progress_view?

...

+# NOTE: This is also deprecated; you should provide a ProgressView instead.
 class _BaseProgressBar(object):

Same here, just wrap __init__:

     @deprecated_method(deprecated_in((1, 17, 0)))
     def __init__(self,

Alternatively if you still need to do it from time to time, we've used a
flag like "_internal=False" and then:

if not _internal:
  warn...

So other than

1) Deprecating things with comments rather than deprecated_in()
2)
* Progress bars are now suppressed again when the environment variable
- - ``BZR_PROGRESS_BAR`` is set to ``none``.
- - (Martin Pool, #339385)
+ ``BZR_PROGRESS_BAR`` is set to ``none``, or when stderr is directed to
+ a terminal.
+ (Martin Pool, #339385, #387717)

^- And this having the wrong meaning.

I approve, but it needs fixing. I would vote "tweak" but I don't know
how to do that.

Review Approve
  review: needs_fixing

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

iEYEARECAAYFAko7mm4ACgkQJdeBCYSNAAPRKwCeJtFuuy6f7RbFEpw7fsNUubkQ
BMgAoICd00+tFOLRsG69P3PFNSGbYVBJ
=Kpqi
-----END PGP SIGNATURE-----

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal
Download full text (3.6 KiB)

>>>>> "martin" == Martin Pool <email address hidden> writes:

    martin> Martin Pool has proposed merging
    martin> lp:~mbp/bzr/387717-progress-bar-tty into lp:bzr.

    martin> Requested reviews:
    martin> bzr-core (bzr-core)

    martin> This stops progress bars being sent to stderr when
    martin> it's not a file.

    martin> (That's not quite the point of bug 387717, but it's still
    martin> useful.)

    martin> There are some more changes in here: ProgressTasks now talk
    martin> directly to their ProgressView rather than to the UIFactory which
    martin> makes them more easily testable in isolation,

    martin> and may let us remove some forwarding methods in the
    martin> UIFactory.

Eerk, that's where bzr-gtk is plugged in: it uses
_progress_all_finished and _progress_updated (I know, I know,
private methods, bad vila, no sugar :).

    martin> (The factory still constructs the task so it can
    martin> arrange to have notifications sent where ever it
    martin> wants.)

At least make UIFactory use a progress_view class attribute or
define _make_progress_view in UIFactory, not in TextUIFactory and
make it public.

Additional bonus points if you can extract the reusable parts
From TextProgressView ;-D

    martin> Some classes whose constructor is deprecated now have
    martin> comments pointing this out.

Hardly discoverable I'm afraid...

I know the deprecation dance is painful, but bzr-gtk is currently
claiming compatibility with bzr 1.6 -> 1.13 and 1.15, if you
remove the forwarding methods above, that would become quite a
challenge. So at least these methods will have to be deprecated
properly (I don't really care about the classes you refer to
here).

<snip/>

    martin> === modified file 'NEWS'
    martin> --- NEWS 2009-06-17 05:05:28 +0000
    martin> +++ NEWS 2009-06-19 08:49:14 +0000
    martin> @@ -21,8 +21,9 @@
    martin> (Neil Martinsen-Burrell, #220067)

    martin> * Progress bars are now suppressed again when the environment
    martin> variable
    martin> - ``BZR_PROGRESS_BAR`` is set to ``none``.
    martin> - (Martin Pool, #339385)
    martin> + ``BZR_PROGRESS_BAR`` is set to ``none``, or when stderr is directed to

Who ate that 'not' ? :)

<snip/>

    martin> @@ -71,7 +70,8 @@
    martin> self._progress_view.clear()

    martin> def _make_progress_view(self):
    martin> - if os.environ.get('BZR_PROGRESS_BAR') in ('text', None, ''):
    martin> + if (os.environ.get('BZR_PROGRESS_BAR') in ('text', None, '')
    martin> + and progress._supports_progress(self.stderr)):
    martin> return TextProgressView(self.stderr)
    martin> else:
    martin> return NullProgressView()

Could we please stop outsmarting the user ? ;-D

Please do:

     def _make_progress_view(self):
- if (os.environ.get('BZR_PROGRESS_BAR') in ('text', None, '')
+ if os.environ.get('BZR_PROGRESS_BAR') == 'text':
+ return TextProgressView(self.stderr)
+ elif (os.environ.get('BZR_PROGRESS_BAR') in (None, '')

I assure you I dig hard, but there is no way to reliably detect
where the output is going when bzr is run under emacs, i...

Read more...

review: Needs Fixing
Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

2009/6/19 fullermd <email address hidden>:
>>  * Progress bars are now suppressed again when the environment variable
>> +  ``BZR_PROGRESS_BAR`` is set to ``none``, or when stderr is directed to
>> +  a terminal.
>> +  (Martin Pool, #339385, #387717)
>
> They're suppressed when stderr *IS* directed to a terminal?  8-}

Good catch, it was backwards.

--
Martin <http://launchpad.net/~mbp/>

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

2009/6/20 John A Meinel <email address hidden>:
> @@ -82,7 +91,9 @@
>         self.total_cnt = None
>         self.current_cnt = None
>         self.msg = ''
> +        # TODO: deprecate passing ui_factory
>         self.ui_factory = ui_factory
> +        self.progress_view = progress_view
>         self.show_pct = False
>         self.show_spinner = True
>         self.show_eta = False,
>
>
> ^- Why not just deprecate it?
>
> if ui_factory is not None:
>  symbol_versioning.warn(
>    'Passing ui_factory' % symbol_versioning.deprecated_in((1, 17, 0)),
>    ' Pass progress_view instead')
>
> or are there still bits that use the ui_factory without going to
> progress_view?

I though there might be some nontrivial work to deprecate it and to
look into how that would impact bzr-gtk, so I wanted to land this
first. (And from Vincent's comments it looks like that's true.)

That is the kind of change I'll put in when it's ready.
>
> ...
>
> +# NOTE: This is also deprecated; you should provide a ProgressView instead.
>  class _BaseProgressBar(object):
>
> Same here, just wrap __init__:
>
>     @deprecated_method(deprecated_in((1, 17, 0)))
>     def __init__(self,
>
> Alternatively if you still need to do it from time to time, we've used a
> flag like "_internal=False" and then:
>
> if not _internal:
>  warn...

All the public concrete constructors are deprecated. You're right I
could and maybe should add an _internal flag so that any callers other
than those subclasses will warn.

>
>
> So other than
>
> 1) Deprecating things with comments rather than deprecated_in()

My cover letter wasn't very clear: these things are already actually
deprecated, it's just that the deprecation marks the constructor not
the class and I wanted it to be more obvious.

> I approve, but it needs fixing. I would vote "tweak" but I don't know
> how to do that.

There's a bug asking for a 'tweak' status. -
https://bugs.edge.launchpad.net/launchpad-code/+bug/373078

--
Martin <http://launchpad.net/~mbp/>

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal
Download full text (4.3 KiB)

2009/6/20 Vincent Ladeuil <email address hidden>:
> Review: Needs Fixing
>
>>>>>> "martin" == Martin Pool <email address hidden> writes:
>
>    martin> Martin Pool has proposed merging
>    martin> lp:~mbp/bzr/387717-progress-bar-tty into lp:bzr.
>
>    martin> Requested reviews:
>    martin>     bzr-core (bzr-core)
>
>    martin> This stops progress bars being sent to stderr when
>    martin> it's not a file.
>
>
>    martin> (That's not quite the point of bug 387717, but it's still
>    martin> useful.)
>
>    martin> There are some more changes in here: ProgressTasks now talk
>    martin> directly to their ProgressView rather than to the UIFactory which
>    martin> makes them more easily testable in isolation,
>
>    martin> and may let us remove some forwarding methods in the
>    martin> UIFactory.
>
> Eerk, that's where bzr-gtk is plugged in: it uses
> _progress_all_finished and _progress_updated (I know, I know,
> private methods, bad vila, no sugar :).

This is why I have some doubt about our past approach of saying
private interfaces can be changed without warning - if people need to
access something, they'll do it regardless of the name and then we
break them. Therefore I think the naming should be just as a guide to
use that particular class, and we need to make the assessment more
broadly.

At any rate I have not actually removed those methods. I'll have a
look at bzr-gtk.

>    martin> (The factory still constructs the task so it can
>    martin> arrange to have notifications sent where ever it
>    martin> wants.)
>
> At least make UIFactory use a progress_view class attribute or
> define _make_progress_view in UIFactory, not in TextUIFactory and
> make it public.
>
> Additional bonus points if you can extract the reusable parts
> >From TextProgressView ;-D

Good ideas.

Though I'll need to think about whether the public should be able to
ask the UIFactory for a ProgressView as opposed to asking it for a
progress bar already bound to a view. How the views get hooked up is
possibly part of the UI's policy...

>    martin> Some classes whose constructor is deprecated now have
>    martin> comments pointing this out.
>
> Hardly discoverable I'm afraid...
>
> I know the deprecation dance is painful, but bzr-gtk is currently
> claiming compatibility with bzr 1.6 -> 1.13 and 1.15, if you
> remove the forwarding methods above, that would become quite a
> challenge. So at least these methods will have to be deprecated
> properly (I don't really care about the classes you refer to
> here).

I didn't deprecate anything, I just added comments on classes whose
constructor is deprecated.

>    martin>      def _make_progress_view(self):
>    martin> - if os.environ.get('BZR_PROGRESS_BAR') in ('text', None, ''):
>    martin> + if (os.environ.get('BZR_PROGRESS_BAR') in ('text', None, '')
>    martin> +            and progress._supports_progress(self.stderr)):
>    martin>              return TextProgressView(self.stderr)
>    martin>          else:
>    martin>              return NullProgressView()
>
>
> Could we please stop outsmarting the user ? ;-D
>
> Please do:
>
>     def _make_progress_view(self):
> -        if (os.environ.get('BZR_P...

Read more...

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

In bzr-gtk I see:

        if getattr(ui.ui_factory, "set_progress_bar_widget", None) is not None:
            # We'are using our own ui, let's tell it to use our widget.
            ui.ui_factory.set_progress_bar_widget(self.progress_widget)

so it can redirect the progress display into the relevant window, and
then it uses _progress_updated and _progress_all_finished to update
them.

So I don't think my patch will break this at all.

If we did deprecate the things I mentioned deprecating, then bzr-gtk
would need to (at least with later bzrlibs) return its own
ProgressView that points to the right progress bar. I don't think it
will be traumatic.

I was in passing thinking about letting it use the activity
notifications to update a throbber and/or to run a nested gtk main
loop.

Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal
Download full text (5.8 KiB)

>>>>> "martin" == Martin Pool <email address hidden> writes:

<snip/>

    >>    martin> and may let us remove some forwarding methods in the
    >>    martin> UIFactory.
    >>
    >> Eerk, that's where bzr-gtk is plugged in: it uses
    >> _progress_all_finished and _progress_updated (I know, I know,
    >> private methods, bad vila, no sugar :).

    martin> This is why I have some doubt about our past approach
    martin> of saying private interfaces can be changed without
    martin> warning - if people need to access something, they'll
    martin> do it regardless of the name and then we break them.

Right. I think our approach about deprecation is sound.

Regarding plugins that use private methods because they couldn't
find a way to achieve something with the public ones, well, we
can't predict *all* needs. But we may encourage people to
*report* what private methods they use so that we can either make
them public or explain how to avoid using them.

<snip/>

    martin> At any rate I have not actually removed those
    martin> methods.

Sure, but you said you will :)

<snip/>

    >> At least make UIFactory use a progress_view class attribute or
    >> define _make_progress_view in UIFactory, not in TextUIFactory and
    >> make it public.
    >>
    >> Additional bonus points if you can extract the reusable parts
    >> >From TextProgressView ;-D

    martin> Good ideas.

Great.

    martin> Though I'll need to think about whether the public
    martin> should be able to ask the UIFactory for a
    martin> ProgressView as opposed to asking it for a progress
    martin> bar already bound to a view.

As I understand your design, there are no more progress bars,
there are progress tasks, a stack of them is maintained
transparently, and the whole stack uses a single report view (I
call it widget).

    martin> How the views get hooked up is possibly part of the
    martin> UI's policy...

Definitely.

There is still a problem specific to bzr-gtk (at least I couldn't
a figure a way bzrlib itself could address it).

Right now, bzr-gtk use a single UIFactory instance (and that's
the design encouraged by bzrlib). From there, the progress widget
is unique too (since UIFactory forwards to it).

Now, imagine that 'bzr viz' starts 'bzr gblame' and that each of
them use a report widget... at one point the progress will
reported by the wrong widget.

Again, I anticipate, it hasn't been encountered yet.

Just a limitation I wanted you to be aware of.

The way that could be addressed in bzr-gtk is to set the progress
widget before each call to bzrlib that requires it (as long as
the bzrlib calls are issued by objects that know, or have a way
to know, which progress widget they should use, that shouldn't be
a too heavy constraint).

And if no more progress widgets are around, bzr-gtk falls back to
creating a top-level window for that purpose.

The following threads give more details:
https://lists.canonical.com/archives/bzr-gtk/2009-June/001824.html
https://lists.canonical.com/archives/bzr-gtk/2009-June/001846.html

    >>    martin> Some classes whose constructor is deprecated now have
    >>    martin> comments pointing this out.
    >>
    >> Ha...

Read more...

Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal
Download full text (5.8 KiB)

>>>>> "martin" == Martin Pool <email address hidden> writes:

<snip/>

    >>    martin> and may let us remove some forwarding methods in the
    >>    martin> UIFactory.
    >>
    >> Eerk, that's where bzr-gtk is plugged in: it uses
    >> _progress_all_finished and _progress_updated (I know, I know,
    >> private methods, bad vila, no sugar :).

    martin> This is why I have some doubt about our past approach
    martin> of saying private interfaces can be changed without
    martin> warning - if people need to access something, they'll
    martin> do it regardless of the name and then we break them.

Right. I think our approach about deprecation is sound.

Regarding plugins that use private methods because they couldn't
find a way to achieve something with the public ones, well, we
can't predict *all* needs. But we may encourage people to
*report* what private methods they use so that we can either make
them public or explain how to avoid using them.

<snip/>

    martin> At any rate I have not actually removed those
    martin> methods.

Sure, but you said you will :)

<snip/>

    >> At least make UIFactory use a progress_view class attribute or
    >> define _make_progress_view in UIFactory, not in TextUIFactory and
    >> make it public.
    >>
    >> Additional bonus points if you can extract the reusable parts
    >> >From TextProgressView ;-D

    martin> Good ideas.

Great.

    martin> Though I'll need to think about whether the public
    martin> should be able to ask the UIFactory for a
    martin> ProgressView as opposed to asking it for a progress
    martin> bar already bound to a view.

As I understand your design, there are no more progress bars,
there are progress tasks, a stack of them is maintained
transparently, and the whole stack uses a single report view (I
call it widget).

    martin> How the views get hooked up is possibly part of the
    martin> UI's policy...

Definitely.

There is still a problem specific to bzr-gtk (at least I couldn't
a figure a way bzrlib itself could address it).

Right now, bzr-gtk use a single UIFactory instance (and that's
the design encouraged by bzrlib). From there, the progress widget
is unique too (since UIFactory forwards to it).

Now, imagine that 'bzr viz' starts 'bzr gblame' and that each of
them use a report widget... at one point the progress will
reported by the wrong widget.

Again, I anticipate, it hasn't been encountered yet.

Just a limitation I wanted you to be aware of.

The way that could be addressed in bzr-gtk is to set the progress
widget before each call to bzrlib that requires it (as long as
the bzrlib calls are issued by objects that know, or have a way
to know, which progress widget they should use, that shouldn't be
a too heavy constraint).

And if no more progress widgets are around, bzr-gtk falls back to
creating a top-level window for that purpose.

The following threads give more details:
https://lists.canonical.com/archives/bzr-gtk/2009-June/001824.html
https://lists.canonical.com/archives/bzr-gtk/2009-June/001846.html

    >>    martin> Some classes whose constructor is deprecated now have
    >>    martin> comments pointing this out.
    >>
    >> Ha...

Read more...

Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal

>>>>> "martin" == Martin Pool <email address hidden> writes:

    martin> In bzr-gtk I see:
    martin> if getattr(ui.ui_factory, "set_progress_bar_widget", None) is not None:
    martin> # We'are using our own ui, let's tell it to use our widget.
    martin> ui.ui_factory.set_progress_bar_widget(self.progress_widget)

    martin> so it can redirect the progress display into the relevant window, and
    martin> then it uses _progress_updated and _progress_all_finished to update
    martin> them.

That's it.

    martin> So I don't think my patch will break this at all.

Not yet :-)

    martin> If we did deprecate the things I mentioned
    martin> deprecating, then bzr-gtk would need to (at least
    martin> with later bzrlibs) return its own ProgressView that
    martin> points to the right progress bar. I don't think it
    martin> will be traumatic.

Agreed.

My point was that it will not be able (as it is now) to support
multiple bzr versions, not a big deal, but one aim of our
deprecation policy is to warn people about what part of their
toolset they should upgrade...

    martin> I was in passing thinking about letting it use the
    martin> activity notifications to update a throbber and/or to
    martin> run a nested gtk main loop.

The GtkProgressBar.update() does:

        while gtk.events_pending():
            gtk.main_iteration()

  Vincent

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

2009/6/22 Vincent Ladeuil <email address hidden>:
>    martin> If we did deprecate the things I mentioned
>    martin> deprecating, then bzr-gtk would need to (at least
>    martin> with later bzrlibs) return its own ProgressView that
>    martin> points to the right progress bar.  I don't think it
>    martin> will be traumatic.
>
> Agreed.
>
> My point was that it will not be able (as it is now) to support
> multiple bzr versions, not a big deal, but one aim of our
> deprecation policy is to warn people about what part of their
> toolset they should upgrade...

I'll try to deprecate them soon so that we can have a long warning period.

>    martin> I was in passing thinking about letting it use the
>    martin> activity notifications to update a throbber and/or to
>    martin> run a nested gtk main loop.
>
> The GtkProgressBar.update() does:
>
>        while gtk.events_pending():
>            gtk.main_iteration()
>
>  Vincent

If it does that from the transport activity notification callback too
it will probably get much more responsive when network IO is running.

--
Martin <http://launchpad.net/~mbp/>

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

> > + # TODO: deprecate passing ui_factory
>
> It would be good to actually do this TODO :)
>
> It shouldn't be hard to do.

I'll probably do it next or soon, but wanted to keep this review small.

> > 145 + 'TERM': 'dumb',
> > 146 + 'LINES': '25',
> > 147 + 'COLUMNS': '80',
>
> (Idle thought... I wonder if the cost of constructing that dictionary every
> test is a measureable cost on the total test suite run, especially as we keep
> adding things to it?)
>
> Part of me wonders if it would be better to just set these variables on
> TestTextProgressView rather than in the base case...

We have occasionally had errors in other cases, particularly in black-box tests but also in others, due to them inadvertently depending on one of these. In a sense every one of those failures is a chance to think about whether that particular case should be tested across various screen setups, but on the whole I think this is an inefficient way to discover them. Let's instead have a standard environment across all tests and then add variations where we need to.

>
> > + task = self.make_task(None, view, 'reticulating splines', 5, 20)
>
> :)
>
> > 248 + self.assertEqual(
> > 249 + r'[####| ] a:b:c 1/2'
> > 250 + , view._render_line())
>
> I think adding a assertRendering method might reduce boilerplate a little, but
> it's not a big issue given there's only a few test methods. (And it's only
> moved code anyway, rather than new code.)

Right.

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

Here's an updated version of this patch. The main functional change is to implement Vincent's suggestion that user settings of environment variables should have effect regardless of what would be autodetected.

Enhance assertIsInstance so you can pass extra explanation.

SilentUI now clearly fails when asked for input rather than looking like it would read from stdin. (I don't think that was ever used or very useful.)

The deprecated bar_type parameter to TextUI was removed.

CLIUIFactory is deprecated and most of the code is fused with TextUIFactory; there's some commentary there on why:

+# Previously, there was CLIUIFactory for dumb terminals, and TextUIFactory for
+# those on smarter terminals. However, there are actually a few independent
+# variables so simple subclassing doesn't make sense. The user may or may not
+# want progress bars, but even if they want them off they may want a rich
+# interface in other ways. And on the other hand input may be redirected from
+# a file so stdin is not a terminal, but we should still try to read input
+# from it and display progress bars etc. Therefore if we're doing a text UI
+# at all, we just use TextUIFactory and it turns some features on or off
+# depending on the detected settings and capabilities.
+#
+# We also use this factory for most blackbox tests, but typically with non-tty
+# streams and TERM=dumb. For api tests, SilentUIFactory is probably
+# appropriate.
+#
+# GUIs may actually choose to subclass TextUIFactory, so unimplemented methods
+# fall back to working through the terminal.
+#
+# SilentUIFactory used to read input from stdin, but not print anything, which
+# now seems like an unhelpful combination. So it will now just fail if asked
+# for input.
+

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

I discovered at least one failure with this change, which is

^^^^[log from bzrlib.tests.bzrdir_implementations.test_bzrdir.TestBreakLock.test_break_lock_branch(BzrDirMetaFormat1)]
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mbp/bzr/387717-progress-bar-tty/bzrlib/tests/bzrdir_implementations/test_bzrdir.py", line 1835, in test_break_lock_branch
    master.bzrdir.break_lock()
  File "/home/mbp/bzr/387717-progress-bar-tty/bzrlib/bzrdir.py", line 125, in break_lock
    thing_to_unlock.break_lock()
  File "/home/mbp/bzr/387717-progress-bar-tty/bzrlib/branch.py", line 118, in break_lock
    self.control_files.break_lock()
  File "/home/mbp/bzr/387717-progress-bar-tty/bzrlib/lockable_files.py", line 140, in break_lock
    self._lock.break_lock()
  File "/home/mbp/bzr/387717-progress-bar-tty/bzrlib/lockdir.py", line 343, in break_lock
    if bzrlib.ui.ui_factory.get_boolean("Break %s" % lock_info):
  File "/home/mbp/bzr/387717-progress-bar-tty/bzrlib/ui/__init__.py", line 158, in get_boolean
    raise NotImplementedError(self.get_boolean)
NotImplementedError: <bound method SilentUIFactory.get_boolean of <bzrlib.ui.SilentUIFactory object at 0x7bad590>>

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

> I discovered at least one failure with this change, which is

Therefore needs-fixing.

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

Another attempt at this branch, originally reviewed here: https://code.edge.launchpad.net/~mbp/bzr/387717-progress-bar-tty/+merge/7669

More could be done here including more model/view separation, but this is a large enough patch for now.

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

John asked in the original review

====
@@ -82,7 +91,9 @@
         self.total_cnt = None
         self.current_cnt = None
         self.msg = ''
+ # TODO: deprecate passing ui_factory
         self.ui_factory = ui_factory
+ self.progress_view = progress_view
         self.show_pct = False
         self.show_spinner = True
         self.show_eta = False,

^- Why not just deprecate it?

if ui_factory is not None:
  symbol_versioning.warn(
    'Passing ui_factory' % symbol_versioning.deprecated_in((1, 17, 0)),
    ' Pass progress_view instead')

or are there still bits that use the ui_factory without going to
progress_view?
=====

Because there are still a few bits like doing pb.note that need to be detangled first; they can't so clearly be seen as operations only on the model of the progress task.

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (50.3 KiB)

Here's the incremental patch (in two parts, to cut out a merge from trunk):

=== modified file 'NEWS'
--- NEWS 2009-06-19 08:49:14 +0000
+++ NEWS 2009-06-22 10:11:31 +0000
@@ -20,9 +20,11 @@
 * Better message in ``bzr split`` error suggesting a rich root format.
   (Neil Martinsen-Burrell, #220067)

-* Progress bars are now suppressed again when the environment variable
- ``BZR_PROGRESS_BAR`` is set to ``none``, or when stderr is directed to
- a terminal.
+* The environment variable ``BZR_PROGRESS_BAR`` set to either ``text`` or ``none``
+ always forces progress bars either on or off respectively. Otherwise,
+ they're turned on if ``TERM`` is not ``dumb`` and stderr is a terminal.
+ bzr always uses the 'text' user interface when run as a command, so
+ ``BZR_USE_TEXT_UI`` is no longer needed.
   (Martin Pool, #339385, #387717)

 Internals
@@ -58,6 +60,12 @@
 API Changes
 ***********

+* ``CLIUIFactory`` is deprecated; use ``TextUIFactory`` instead if you
+ need to subclass or create a specific class, or better yet the existing
+ ``make_ui_for_terminal``. ``SilentUIFactory`` is now more clearly
+ defined to raise errors when asked to read input, rather than taking it
+ from stdin. (Martin Pool)
+
 * ProgressTasks now prefer to talk direct to their ProgressView not to the
   UIFactory.
   (Martin Pool)

=== modified file 'bzrlib/progress.py'
--- bzrlib/progress.py 2009-06-19 08:37:24 +0000
+++ bzrlib/progress.py 2009-06-22 10:27:55 +0000
@@ -50,12 +50,15 @@

     This doesn't check the policy for whether we *should* use them.
     """
- # XXX: This is a bit redundant with make_ui_for_terminal.
     isatty = getattr(f, 'isatty', None)
     if isatty is None:
         return False
     if not isatty():
         return False
+ # The following case also handles Win32 - on that platform $TERM is
+ # typically never set, so the case None is treated as a smart terminal,
+ # not dumb. <https://bugs.launchpad.net/bugs/334808> win32 files do have
+ # isatty methods that return true.
     if os.environ.get('TERM') == 'dumb':
         # e.g. emacs compile window
         return False

=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py 2009-06-19 06:49:55 +0000
+++ bzrlib/tests/__init__.py 2009-06-22 09:16:26 +0000
@@ -102,6 +102,7 @@
                           TestLoader,
                           )
 from bzrlib.tests.treeshape import build_tree_contents
+from bzrlib.ui.text import TextUIFactory
 import bzrlib.version_info_formats.format_custom
 from bzrlib.workingtree import WorkingTree, WorkingTreeFormat2

@@ -702,7 +703,7 @@
             return setattr(self._cstring, name, val)

-class TestUIFactory(ui.CLIUIFactory):
+class TestUIFactory(TextUIFactory):
     """A UI Factory for testing.

     Hide the progress bar but emit note()s.
@@ -1093,11 +1094,17 @@
                          osutils.realpath(path2),
                          "apparent paths:\na = %s\nb = %s\n," % (path1, path2))

- def assertIsInstance(self, obj, kls):
- """Fail if obj is not an instance of kls"""
+ def assertIsInstance(self, obj, kls, msg=None):
+ """Fail if obj is not an instance of kls
+ ...

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

Robert did a review on irc, and I've done the points, therefore now submitting this.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2009-07-22 01:36:42 +0000
3+++ NEWS 2009-07-22 08:35:19 +0000
4@@ -30,6 +30,13 @@
5 * BranchBuilder now accepts timezone to avoid test failures in countries far
6 from GMT. (Vincent Ladeuil, #397716)
7
8+* The environment variable ``BZR_PROGRESS_BAR`` set to either ``text`` or ``none``
9+ always forces progress bars either on or off respectively. Otherwise,
10+ they're turned on if ``TERM`` is not ``dumb`` and stderr is a terminal.
11+ bzr always uses the 'text' user interface when run as a command, so
12+ ``BZR_USE_TEXT_UI`` is no longer needed.
13+ (Martin Pool, #339385, #387717)
14+
15 * ``bzr commit`` no longer saves the unversioning of missing files until
16 the commit has completed on the branch. This means that aborting a
17 commit that found a missing file will leave the tree unedited.
18@@ -74,9 +81,20 @@
19 API Changes
20 ***********
21
22+* ``CLIUIFactory`` is deprecated; use ``TextUIFactory`` instead if you
23+ need to subclass or create a specific class, or better yet the existing
24+ ``make_ui_for_terminal``. ``SilentUIFactory`` is clarified to do no
25+ user interaction at all, rather than trying to read from stdin but not
26+ writing any output, which would be strange if reading prompts or
27+ passwords. (Martin Pool)
28+
29 * New TransformPreview.commit() allows committing without a working tree.
30 (Aaron Bentley)
31
32+* ProgressTasks now prefer to talk direct to their ProgressView not to the
33+ UIFactory.
34+ (Martin Pool)
35+
36 Internals
37 *********
38
39
40=== modified file 'bzrlib/progress.py'
41--- bzrlib/progress.py 2009-07-01 10:54:51 +0000
42+++ bzrlib/progress.py 2009-07-22 08:35:19 +0000
43@@ -37,18 +37,23 @@
44 )
45
46
47-# XXX: deprecated; can be removed when the ProgressBar factory is removed
48 def _supports_progress(f):
49- """Detect if we can use pretty progress bars on the output stream f.
50+ """Detect if we can use pretty progress bars on file F.
51
52 If this returns true we expect that a human may be looking at that
53 output, and that we can repaint a line to update it.
54+
55+ This doesn't check the policy for whether we *should* use them.
56 """
57 isatty = getattr(f, 'isatty', None)
58 if isatty is None:
59 return False
60 if not isatty():
61 return False
62+ # The following case also handles Win32 - on that platform $TERM is
63+ # typically never set, so the case None is treated as a smart terminal,
64+ # not dumb. <https://bugs.launchpad.net/bugs/334808> win32 files do have
65+ # isatty methods that return true.
66 if os.environ.get('TERM') == 'dumb':
67 # e.g. emacs compile window
68 return False
69@@ -66,9 +71,16 @@
70 will not necessarily respect all these fields.
71 """
72
73- def __init__(self, parent_task=None, ui_factory=None):
74+ def __init__(self, parent_task=None, ui_factory=None, progress_view=None):
75 """Construct a new progress task.
76
77+ :param parent_task: Enclosing ProgressTask or None.
78+
79+ :param progress_view: ProgressView to display this ProgressTask.
80+
81+ :param ui_factory: The UI factory that will display updates;
82+ deprecated in favor of passing progress_view directly.
83+
84 Normally you should not call this directly but rather through
85 `ui_factory.nested_progress_bar`.
86 """
87@@ -77,7 +89,9 @@
88 self.total_cnt = None
89 self.current_cnt = None
90 self.msg = ''
91+ # TODO: deprecate passing ui_factory
92 self.ui_factory = ui_factory
93+ self.progress_view = progress_view
94 self.show_pct = False
95 self.show_spinner = True
96 self.show_eta = False,
97@@ -96,16 +110,23 @@
98 self.current_cnt = current_cnt
99 if total_cnt:
100 self.total_cnt = total_cnt
101- self.ui_factory._progress_updated(self)
102+ if self.progress_view:
103+ self.progress_view.show_progress(self)
104+ else:
105+ self.ui_factory._progress_updated(self)
106
107 def tick(self):
108 self.update(self.msg)
109
110 def finished(self):
111- self.ui_factory._progress_finished(self)
112+ if self.progress_view:
113+ self.progress_view.task_finished(self)
114+ else:
115+ self.ui_factory._progress_finished(self)
116
117 def make_sub_task(self):
118- return ProgressTask(self, self.ui_factory)
119+ return ProgressTask(self, ui_factory=self.ui_factory,
120+ progress_view=self.progress_view)
121
122 def _overall_completion_fraction(self, child_fraction=0.0):
123 """Return fractional completion of this task and its parents
124@@ -134,7 +155,10 @@
125
126 def clear(self):
127 # XXX: shouldn't be here; put it in mutter or the ui instead
128- self.ui_factory.clear_term()
129+ if self.progress_view:
130+ self.progress_view.clear()
131+ else:
132+ self.ui_factory.clear_term()
133
134
135 @deprecated_function(deprecated_in((1, 16, 0)))
136@@ -161,6 +185,7 @@
137 return _progress_bar_types[requested_bar_type](to_file=to_file, **kwargs)
138
139
140+# NOTE: This is also deprecated; you should provide a ProgressView instead.
141 class _BaseProgressBar(object):
142
143 def __init__(self,
144@@ -448,6 +473,8 @@
145 #self.to_file.flush()
146
147
148+
149+# DEPRECATED
150 class ChildProgress(_BaseProgressBar):
151 """A progress indicator that pushes its data to the parent"""
152
153
154=== modified file 'bzrlib/tests/__init__.py'
155--- bzrlib/tests/__init__.py 2009-07-20 04:22:47 +0000
156+++ bzrlib/tests/__init__.py 2009-07-22 08:35:19 +0000
157@@ -102,6 +102,7 @@
158 TestLoader,
159 )
160 from bzrlib.tests.treeshape import build_tree_contents
161+from bzrlib.ui.text import TextUIFactory
162 import bzrlib.version_info_formats.format_custom
163 from bzrlib.workingtree import WorkingTree, WorkingTreeFormat2
164
165@@ -702,7 +703,7 @@
166 return setattr(self._cstring, name, val)
167
168
169-class TestUIFactory(ui.CLIUIFactory):
170+class TestUIFactory(TextUIFactory):
171 """A UI Factory for testing.
172
173 Hide the progress bar but emit note()s.
174@@ -1093,11 +1094,17 @@
175 osutils.realpath(path2),
176 "apparent paths:\na = %s\nb = %s\n," % (path1, path2))
177
178- def assertIsInstance(self, obj, kls):
179- """Fail if obj is not an instance of kls"""
180+ def assertIsInstance(self, obj, kls, msg=None):
181+ """Fail if obj is not an instance of kls
182+
183+ :param msg: Supplementary message to show if the assertion fails.
184+ """
185 if not isinstance(obj, kls):
186- self.fail("%r is an instance of %s rather than %s" % (
187- obj, obj.__class__, kls))
188+ m = "%r is an instance of %s rather than %s" % (
189+ obj, obj.__class__, kls)
190+ if msg:
191+ m += ": " + msg
192+ self.fail(m)
193
194 def expectFailure(self, reason, assertion, *args, **kwargs):
195 """Invoke a test, expecting it to fail for the given reason.
196@@ -1325,6 +1332,13 @@
197 'BZR_PROGRESS_BAR': None,
198 'BZR_LOG': None,
199 'BZR_PLUGIN_PATH': None,
200+ # Make sure that any text ui tests are consistent regardless of
201+ # the environment the test case is run in; you may want tests that
202+ # test other combinations. 'dumb' is a reasonable guess for tests
203+ # going to a pipe or a StringIO.
204+ 'TERM': 'dumb',
205+ 'LINES': '25',
206+ 'COLUMNS': '80',
207 # SSH Agent
208 'SSH_AUTH_SOCK': None,
209 # Proxies
210
211=== modified file 'bzrlib/tests/per_branch/test_break_lock.py'
212--- bzrlib/tests/per_branch/test_break_lock.py 2009-07-10 05:49:34 +0000
213+++ bzrlib/tests/per_branch/test_break_lock.py 2009-07-22 08:35:20 +0000
214@@ -1,4 +1,4 @@
215-# Copyright (C) 2006 Canonical Ltd
216+# Copyright (C) 2006, 2009 Canonical Ltd
217 #
218 # This program is free software; you can redistribute it and/or modify
219 # it under the terms of the GNU General Public License as published by
220@@ -22,6 +22,9 @@
221 import bzrlib.errors as errors
222 from bzrlib.tests import TestCase, TestCaseWithTransport, TestNotApplicable
223 from bzrlib.tests.per_branch.test_branch import TestCaseWithBranch
224+from bzrlib.ui import (
225+ CannedInputUIFactory,
226+ )
227
228
229 class TestBreakLock(TestCaseWithBranch):
230@@ -35,7 +38,6 @@
231 # ours
232 self.old_factory = bzrlib.ui.ui_factory
233 self.addCleanup(self.restoreFactory)
234- bzrlib.ui.ui_factory = bzrlib.ui.SilentUIFactory()
235
236 def restoreFactory(self):
237 bzrlib.ui.ui_factory = self.old_factory
238@@ -59,7 +61,7 @@
239 other_instance = self.branch.repository.bzrdir.open_repository()
240 if not other_instance.get_physical_lock_status():
241 raise TestNotApplicable("Repository does not lock persistently.")
242- bzrlib.ui.ui_factory.stdin = StringIO("y\n")
243+ bzrlib.ui.ui_factory = CannedInputUIFactory([True])
244 try:
245 self.unused_branch.break_lock()
246 except NotImplementedError:
247@@ -71,7 +73,7 @@
248 def test_locked(self):
249 # break_lock when locked should unlock the branch and repo
250 self.branch.lock_write()
251- bzrlib.ui.ui_factory.stdin = StringIO("y\ny\n")
252+ bzrlib.ui.ui_factory = CannedInputUIFactory([True, True])
253 try:
254 self.unused_branch.break_lock()
255 except NotImplementedError:
256@@ -90,7 +92,7 @@
257 # this branch does not support binding.
258 return
259 master.lock_write()
260- bzrlib.ui.ui_factory.stdin = StringIO("y\ny\n")
261+ bzrlib.ui.ui_factory = CannedInputUIFactory([True, True])
262 try:
263 self.unused_branch.break_lock()
264 except NotImplementedError:
265
266=== modified file 'bzrlib/tests/per_bzrdir/test_bzrdir.py'
267--- bzrlib/tests/per_bzrdir/test_bzrdir.py 2009-07-10 06:45:04 +0000
268+++ bzrlib/tests/per_bzrdir/test_bzrdir.py 2009-07-22 08:35:20 +0000
269@@ -57,6 +57,9 @@
270 from bzrlib.trace import mutter
271 from bzrlib.transport import get_transport
272 from bzrlib.transport.local import LocalTransport
273+from bzrlib.ui import (
274+ CannedInputUIFactory,
275+ )
276 from bzrlib.upgrade import upgrade
277 from bzrlib.remote import RemoteBzrDir, RemoteRepository
278 from bzrlib.repofmt import weaverepo
279@@ -1774,7 +1777,6 @@
280 # ours
281 self.old_factory = bzrlib.ui.ui_factory
282 self.addCleanup(self.restoreFactory)
283- bzrlib.ui.ui_factory = bzrlib.ui.SilentUIFactory()
284
285 def restoreFactory(self):
286 bzrlib.ui.ui_factory = self.old_factory
287@@ -1800,7 +1802,7 @@
288 return
289 # only one yes needed here: it should only be unlocking
290 # the repo
291- bzrlib.ui.ui_factory.stdin = StringIO("y\n")
292+ bzrlib.ui.ui_factory = CannedInputUIFactory([True])
293 try:
294 repo.bzrdir.break_lock()
295 except NotImplementedError:
296@@ -1831,17 +1833,19 @@
297 # two yes's : branch and repository. If the repo in this
298 # dir is inappropriately accessed, 3 will be needed, and
299 # we'll see that because the stream will be fully consumed
300- bzrlib.ui.ui_factory.stdin = StringIO("y\ny\ny\n")
301+ bzrlib.ui.ui_factory = CannedInputUIFactory([True, True, True])
302 # determine if the repository will have been locked;
303 this_repo_locked = \
304 thisdir.open_repository().get_physical_lock_status()
305 master.bzrdir.break_lock()
306 if this_repo_locked:
307 # only two ys should have been read
308- self.assertEqual("y\n", bzrlib.ui.ui_factory.stdin.read())
309+ self.assertEqual([True],
310+ bzrlib.ui.ui_factory.responses)
311 else:
312 # only one y should have been read
313- self.assertEqual("y\ny\n", bzrlib.ui.ui_factory.stdin.read())
314+ self.assertEqual([True, True],
315+ bzrlib.ui.ui_factory.responses)
316 # we should be able to lock a newly opened branch now
317 branch = master.bzrdir.open_branch()
318 branch.lock_write()
319@@ -1865,7 +1869,7 @@
320 tree = self.make_branch_and_tree('.')
321 tree.lock_write()
322 # three yes's : tree, branch and repository.
323- bzrlib.ui.ui_factory.stdin = StringIO("y\ny\ny\ny\n")
324+ bzrlib.ui.ui_factory = CannedInputUIFactory([True, True, True])
325 try:
326 tree.bzrdir.break_lock()
327 except (NotImplementedError, errors.LockActive):
328@@ -1875,7 +1879,8 @@
329 # object.
330 tree.unlock()
331 return
332- self.assertEqual("y\n", bzrlib.ui.ui_factory.stdin.read())
333+ self.assertEqual([True],
334+ bzrlib.ui.ui_factory.responses)
335 lock_tree = tree.bzrdir.open_workingtree()
336 lock_tree.lock_write()
337 lock_tree.unlock()
338
339=== modified file 'bzrlib/tests/per_repository/test_break_lock.py'
340--- bzrlib/tests/per_repository/test_break_lock.py 2009-03-23 14:59:43 +0000
341+++ bzrlib/tests/per_repository/test_break_lock.py 2009-07-22 08:35:20 +0000
342@@ -23,6 +23,9 @@
343 from bzrlib.tests.per_repository.test_repository import TestCaseWithRepository
344 from bzrlib.transport import get_transport
345 from bzrlib.workingtree import WorkingTree
346+from bzrlib.ui import (
347+ CannedInputUIFactory,
348+ )
349
350
351 class TestBreakLock(TestCaseWithRepository):
352@@ -36,8 +39,7 @@
353 # ours
354 self.old_factory = bzrlib.ui.ui_factory
355 self.addCleanup(self.restoreFactory)
356- bzrlib.ui.ui_factory = bzrlib.ui.SilentUIFactory()
357- bzrlib.ui.ui_factory.stdin = StringIO("y\n")
358+ bzrlib.ui.ui_factory = CannedInputUIFactory([True])
359
360 def restoreFactory(self):
361 bzrlib.ui.ui_factory = self.old_factory
362
363=== modified file 'bzrlib/tests/per_repository_reference/test_break_lock.py'
364--- bzrlib/tests/per_repository_reference/test_break_lock.py 2009-03-23 14:59:43 +0000
365+++ bzrlib/tests/per_repository_reference/test_break_lock.py 2009-07-22 08:35:20 +0000
366@@ -21,6 +21,9 @@
367 from bzrlib.tests.per_repository_reference import (
368 TestCaseWithExternalReferenceRepository,
369 )
370+from bzrlib.ui import (
371+ CannedInputUIFactory,
372+ )
373
374
375 class TestBreakLock(TestCaseWithExternalReferenceRepository):
376@@ -39,12 +42,8 @@
377 # 'lock_write' has not taken a physical mutex out.
378 repo.unlock()
379 return
380- # we want a UI factory that accepts canned input for the tests:
381- # while SilentUIFactory still accepts stdin, we need to customise
382- # ours
383 self.old_factory = bzrlib.ui.ui_factory
384 self.addCleanup(self.restoreFactory)
385- bzrlib.ui.ui_factory = bzrlib.ui.SilentUIFactory()
386- bzrlib.ui.ui_factory.stdin = StringIO("y\n")
387+ bzrlib.ui.ui_factory = CannedInputUIFactory([True])
388 unused_repo.break_lock()
389 self.assertRaises(errors.LockBroken, repo.unlock)
390
391=== modified file 'bzrlib/tests/per_workingtree/test_break_lock.py'
392--- bzrlib/tests/per_workingtree/test_break_lock.py 2009-07-10 07:14:02 +0000
393+++ bzrlib/tests/per_workingtree/test_break_lock.py 2009-07-22 08:35:20 +0000
394@@ -30,12 +30,8 @@
395 super(TestBreakLock, self).setUp()
396 self.unused_workingtree = self.make_branch_and_tree('.')
397 self.workingtree = self.unused_workingtree.bzrdir.open_workingtree()
398- # we want a UI factory that accepts canned input for the tests:
399- # while SilentUIFactory still accepts stdin, we need to customise
400- # ours
401 self.old_factory = bzrlib.ui.ui_factory
402 self.addCleanup(self.restoreFactory)
403- bzrlib.ui.ui_factory = bzrlib.ui.SilentUIFactory()
404
405 def restoreFactory(self):
406 bzrlib.ui.ui_factory = self.old_factory
407@@ -52,16 +48,14 @@
408 # if the workingtree isn't locked - and the easiest way
409 # to see if that happened is to lock the repo.
410 self.workingtree.branch.repository.lock_write()
411- stdin = StringIO("y\n")
412- bzrlib.ui.ui_factory.stdin = stdin
413+ bzrlib.ui.ui_factory = bzrlib.ui.CannedInputUIFactory([True])
414 try:
415 self.unused_workingtree.break_lock()
416 except NotImplementedError:
417 # workingtree does not support break_lock
418 self.workingtree.branch.repository.unlock()
419 return
420- remaining = stdin.read()
421- if remaining == 'y\n':
422+ if bzrlib.ui.ui_factory.responses == [True]:
423 raise TestNotApplicable("repository does not physically lock.")
424 self.assertRaises(errors.LockBroken,
425 self.workingtree.branch.repository.unlock)
426@@ -69,7 +63,7 @@
427 def test_locked(self):
428 # break_lock when locked should
429 self.workingtree.lock_write()
430- bzrlib.ui.ui_factory.stdin = StringIO("y\ny\ny\n")
431+ bzrlib.ui.ui_factory = bzrlib.ui.CannedInputUIFactory([True, True, True])
432 try:
433 self.unused_workingtree.break_lock()
434 except (NotImplementedError, errors.LockActive):
435
436=== modified file 'bzrlib/tests/test_config.py'
437--- bzrlib/tests/test_config.py 2009-07-02 08:59:16 +0000
438+++ bzrlib/tests/test_config.py 2009-07-22 08:35:19 +0000
439@@ -1,5 +1,4 @@
440-# Copyright (C) 2005, 2006, 2008 Canonical Ltd
441-# Authors: Robert Collins <robert.collins@canonical.com>
442+# Copyright (C) 2005, 2006, 2008, 2009 Canonical Ltd
443 #
444 # This program is free software; you can redistribute it and/or modify
445 # it under the terms of the GNU General Public License as published by
446@@ -1561,8 +1560,9 @@
447 """))
448 entered_password = 'typed-by-hand'
449 stdout = tests.StringIOWrapper()
450+ stderr = tests.StringIOWrapper()
451 ui.ui_factory = tests.TestUIFactory(stdin=entered_password + '\n',
452- stdout=stdout)
453+ stdout=stdout, stderr=stderr)
454
455 # Since the password defined in the authentication config is ignored,
456 # the user is prompted
457@@ -1582,8 +1582,10 @@
458 """))
459 entered_password = 'typed-by-hand'
460 stdout = tests.StringIOWrapper()
461+ stderr = tests.StringIOWrapper()
462 ui.ui_factory = tests.TestUIFactory(stdin=entered_password + '\n',
463- stdout=stdout)
464+ stdout=stdout,
465+ stderr=stderr)
466
467 # Since the password defined in the authentication config is ignored,
468 # the user is prompted
469
470=== modified file 'bzrlib/tests/test_ftp_transport.py'
471--- bzrlib/tests/test_ftp_transport.py 2009-04-10 15:58:09 +0000
472+++ bzrlib/tests/test_ftp_transport.py 2009-07-22 08:35:19 +0000
473@@ -90,27 +90,24 @@
474 configuration.""",
475 self.get_server().add_user(getpass.getuser(), self.password)
476 t = self.get_transport()
477- ui.ui_factory = tests.TestUIFactory(stdin=self.password + '\n',
478- stdout=tests.StringIOWrapper())
479+ ui.ui_factory = ui.CannedInputUIFactory([self.password])
480 # Issue a request to the server to connect
481 t.put_bytes('foo', 'test bytes\n')
482 self.assertEqual('test bytes\n', t.get_bytes('foo'))
483 # Only the password should've been read
484- self.assertEqual('', ui.ui_factory.stdin.readline())
485+ ui.ui_factory.assert_all_input_consumed()
486
487 def test_prompt_for_password(self):
488 t = self.get_transport()
489- ui.ui_factory = tests.TestUIFactory(stdin=self.password+'\n',
490- stdout=tests.StringIOWrapper())
491+ ui.ui_factory = ui.CannedInputUIFactory([self.password])
492 # Issue a request to the server to connect
493 t.has('whatever/not/existing')
494 # stdin should be empty (the provided password have been consumed)
495- self.assertEqual('', ui.ui_factory.stdin.readline())
496+ ui.ui_factory.assert_all_input_consumed()
497
498 def test_no_prompt_for_password_when_using_auth_config(self):
499 t = self.get_transport()
500- ui.ui_factory = tests.TestUIFactory(stdin='precious\n',
501- stdout=tests.StringIOWrapper())
502+ ui.ui_factory = ui.CannedInputUIFactory([])
503 # Create a config file with the right password
504 conf = config.AuthenticationConfig()
505 conf._get_config().update({'ftptest': {'scheme': 'ftp',
506@@ -120,8 +117,6 @@
507 # Issue a request to the server to connect
508 t.put_bytes('foo', 'test bytes\n')
509 self.assertEqual('test bytes\n', t.get_bytes('foo'))
510- # stdin should have been left untouched
511- self.assertEqual('precious\n', ui.ui_factory.stdin.readline())
512
513 def test_empty_password(self):
514 # Override the default user/password from setUp
515@@ -129,11 +124,9 @@
516 self.password = ''
517 self.get_server().add_user(self.user, self.password)
518 t = self.get_transport()
519- ui.ui_factory = tests.TestUIFactory(stdin=self.password+'\n',
520- stdout=tests.StringIOWrapper())
521+ ui.ui_factory = ui.CannedInputUIFactory([self.password])
522 # Issue a request to the server to connect
523 t.has('whatever/not/existing')
524 # stdin should be empty (the provided password have been consumed),
525 # even if the password is empty, it's followed by a newline.
526- self.assertEqual('', ui.ui_factory.stdin.readline())
527-
528+ ui.ui_factory.assert_all_input_consumed()
529
530=== modified file 'bzrlib/tests/test_lockable_files.py'
531--- bzrlib/tests/test_lockable_files.py 2009-05-11 07:14:58 +0000
532+++ bzrlib/tests/test_lockable_files.py 2009-07-22 08:35:19 +0000
533@@ -157,8 +157,7 @@
534 l2 = self.get_lockable()
535 orig_factory = bzrlib.ui.ui_factory
536 # silent ui - no need for stdout
537- bzrlib.ui.ui_factory = bzrlib.ui.SilentUIFactory()
538- bzrlib.ui.ui_factory.stdin = StringIO("y\n")
539+ bzrlib.ui.ui_factory = bzrlib.ui.CannedInputUIFactory([True])
540 try:
541 l2.break_lock()
542 finally:
543
544=== modified file 'bzrlib/tests/test_lockdir.py'
545--- bzrlib/tests/test_lockdir.py 2009-05-08 15:58:00 +0000
546+++ bzrlib/tests/test_lockdir.py 2009-07-22 08:35:19 +0000
547@@ -561,9 +561,7 @@
548 # do this without IO redirection to ensure it doesn't prompt.
549 self.assertRaises(AssertionError, ld1.break_lock)
550 orig_factory = bzrlib.ui.ui_factory
551- # silent ui - no need for stdout
552- bzrlib.ui.ui_factory = bzrlib.ui.SilentUIFactory()
553- bzrlib.ui.ui_factory.stdin = StringIO("y\n")
554+ bzrlib.ui.ui_factory = bzrlib.ui.CannedInputUIFactory([True])
555 try:
556 ld2.break_lock()
557 self.assertRaises(LockBroken, ld1.unlock)
558
559=== modified file 'bzrlib/tests/test_pack_repository.py'
560--- bzrlib/tests/test_pack_repository.py 2009-06-26 09:24:34 +0000
561+++ bzrlib/tests/test_pack_repository.py 2009-07-22 08:35:20 +0000
562@@ -517,8 +517,7 @@
563 def restoreFactory():
564 ui.ui_factory = old_factory
565 self.addCleanup(restoreFactory)
566- ui.ui_factory = ui.SilentUIFactory()
567- ui.ui_factory.stdin = StringIO("y\n")
568+ ui.ui_factory = ui.CannedInputUIFactory([True])
569
570 def test_break_lock_breaks_physical_lock(self):
571 repo = self.make_repository('.', format=self.get_format())
572
573=== modified file 'bzrlib/tests/test_progress.py'
574--- bzrlib/tests/test_progress.py 2009-06-10 03:56:49 +0000
575+++ bzrlib/tests/test_progress.py 2009-07-22 08:35:20 +0000
576@@ -14,14 +14,21 @@
577 # along with this program; if not, write to the Free Software
578 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
579
580+
581 import os
582 from StringIO import StringIO
583
584 from bzrlib import errors
585-from bzrlib.tests import TestCase
586+from bzrlib.progress import (
587+ ProgressTask,
588+ )
589 from bzrlib.symbol_versioning import (
590 deprecated_in,
591 )
592+from bzrlib.tests import TestCase
593+from bzrlib.ui.text import (
594+ TextProgressView,
595+ )
596
597
598 class _TTYStringIO(StringIO):
599@@ -36,3 +43,72 @@
600
601 def isatty(self):
602 return False
603+
604+
605+class TestTextProgressView(TestCase):
606+ """Tests for text display of progress bars.
607+
608+ These try to exercise the progressview independently of its construction,
609+ which is arranged by the TextUIFactory.
610+ """
611+ # The ProgressTask now connects directly to the ProgressView, so we can
612+ # check them independently of the factory or of the determination of what
613+ # view to use.
614+
615+ def make_view(self):
616+ out = StringIO()
617+ view = TextProgressView(out)
618+ view._width = 80
619+ return out, view
620+
621+ def make_task(self, parent_task, view, msg, curr, total):
622+ # would normally be done by UIFactory; is done here so that we don't
623+ # have to have one.
624+ task = ProgressTask(parent_task, progress_view=view)
625+ task.msg = msg
626+ task.current_cnt = curr
627+ task.total_cnt = total
628+ return task
629+
630+ def test_render_progress_easy(self):
631+ """Just one task and one quarter done"""
632+ out, view = self.make_view()
633+ task = self.make_task(None, view, 'reticulating splines', 5, 20)
634+ view.show_progress(task)
635+ self.assertEqual(
636+'\r[####/ ] reticulating splines 5/20 \r'
637+ , out.getvalue())
638+
639+ def test_render_progress_nested(self):
640+ """Tasks proportionally contribute to overall progress"""
641+ out, view = self.make_view()
642+ task = self.make_task(None, view, 'reticulating splines', 0, 2)
643+ task2 = self.make_task(task, view, 'stage2', 1, 2)
644+ view.show_progress(task2)
645+ # so we're in the first half of the main task, and half way through
646+ # that
647+ self.assertEqual(
648+r'[####- ] reticulating splines:stage2 1/2'
649+ , view._render_line())
650+ # if the nested task is complete, then we're all the way through the
651+ # first half of the overall work
652+ task2.update('stage2', 2, 2)
653+ self.assertEqual(
654+r'[#########\ ] reticulating splines:stage2 2/2'
655+ , view._render_line())
656+
657+ def test_render_progress_sub_nested(self):
658+ """Intermediate tasks don't mess up calculation."""
659+ out, view = self.make_view()
660+ task_a = ProgressTask(None, progress_view=view)
661+ task_a.update('a', 0, 2)
662+ task_b = ProgressTask(task_a, progress_view=view)
663+ task_b.update('b')
664+ task_c = ProgressTask(task_b, progress_view=view)
665+ task_c.update('c', 1, 2)
666+ # the top-level task is in its first half; the middle one has no
667+ # progress indication, just a label; and the bottom one is half done,
668+ # so the overall fraction is 1/4
669+ self.assertEqual(
670+ r'[####| ] a:b:c 1/2'
671+ , view._render_line())
672
673=== modified file 'bzrlib/tests/test_selftest.py'
674--- bzrlib/tests/test_selftest.py 2009-07-20 04:22:47 +0000
675+++ bzrlib/tests/test_selftest.py 2009-07-22 08:35:20 +0000
676@@ -1687,8 +1687,15 @@
677 def test_assert_isinstance(self):
678 self.assertIsInstance(2, int)
679 self.assertIsInstance(u'', basestring)
680- self.assertRaises(AssertionError, self.assertIsInstance, None, int)
681+ e = self.assertRaises(AssertionError, self.assertIsInstance, None, int)
682+ self.assertEquals(str(e),
683+ "None is an instance of <type 'NoneType'> rather than <type 'int'>")
684 self.assertRaises(AssertionError, self.assertIsInstance, 23.3, int)
685+ e = self.assertRaises(AssertionError,
686+ self.assertIsInstance, None, int, "it's just not")
687+ self.assertEquals(str(e),
688+ "None is an instance of <type 'NoneType'> rather than <type 'int'>"
689+ ": it's just not")
690
691 def test_assertEndsWith(self):
692 self.assertEndsWith('foo', 'oo')
693
694=== modified file 'bzrlib/tests/test_smtp_connection.py'
695--- bzrlib/tests/test_smtp_connection.py 2009-03-25 18:43:31 +0000
696+++ bzrlib/tests/test_smtp_connection.py 2009-07-22 08:35:20 +0000
697@@ -1,4 +1,4 @@
698-# Copyright (C) 2005, 2007 Canonical Ltd
699+# Copyright (C) 2005, 2007, 2009 Canonical Ltd
700 #
701 # This program is free software; you can redistribute it and/or modify
702 # it under the terms of the GNU General Public License as published by
703@@ -136,12 +136,9 @@
704 smtp_factory=factory)
705 self.assertIs(None, conn._smtp_password)
706
707- ui.ui_factory = tests.TestUIFactory(stdin=password + '\n',
708- stdout=tests.StringIOWrapper())
709+ ui.ui_factory = ui.CannedInputUIFactory([password])
710 conn._connect()
711 self.assertEqual(password, conn._smtp_password)
712- # stdin should be empty (the provided password have been consumed)
713- self.assertEqual('', ui.ui_factory.stdin.readline())
714
715 def test_smtp_password_from_auth_config(self):
716 user = 'joe'
717
718=== modified file 'bzrlib/tests/test_ui.py'
719--- bzrlib/tests/test_ui.py 2009-07-02 09:09:35 +0000
720+++ bzrlib/tests/test_ui.py 2009-07-22 08:35:20 +0000
721@@ -31,7 +31,22 @@
722 from bzrlib.symbol_versioning import (
723 deprecated_in,
724 )
725-from bzrlib.tests.test_progress import _TTYStringIO
726+from bzrlib.tests import (
727+ TestCase,
728+ TestUIFactory,
729+ StringIOWrapper,
730+ )
731+from bzrlib.tests.test_progress import (
732+ _NonTTYStringIO,
733+ _TTYStringIO,
734+ )
735+from bzrlib.ui import (
736+ CannedInputUIFactory,
737+ CLIUIFactory,
738+ SilentUIFactory,
739+ UIFactory,
740+ make_ui_for_terminal,
741+ )
742 from bzrlib.ui.text import (
743 NullProgressView,
744 TextProgressView,
745@@ -41,20 +56,6 @@
746
747 class UITests(tests.TestCase):
748
749- def test_silent_factory(self):
750- ui = _mod_ui.SilentUIFactory()
751- stdout = StringIO()
752- self.assertEqual(None,
753- self.apply_redirected(None, stdout, stdout,
754- ui.get_password))
755- self.assertEqual('', stdout.getvalue())
756- self.assertEqual(None,
757- self.apply_redirected(None, stdout, stdout,
758- ui.get_password,
759- u'Hello\u1234 %(user)s',
760- user=u'some\u1234'))
761- self.assertEqual('', stdout.getvalue())
762-
763 def test_text_factory_ascii_password(self):
764 ui = tests.TestUIFactory(stdin='secret\n',
765 stdout=tests.StringIOWrapper(),
766@@ -102,21 +103,52 @@
767 def test_progress_construction(self):
768 """TextUIFactory constructs the right progress view.
769 """
770- os.environ['BZR_PROGRESS_BAR'] = 'none'
771- self.assertIsInstance(TextUIFactory()._progress_view,
772- NullProgressView)
773-
774- os.environ['BZR_PROGRESS_BAR'] = 'text'
775- self.assertIsInstance(TextUIFactory()._progress_view,
776- TextProgressView)
777-
778- os.environ['BZR_PROGRESS_BAR'] = 'text'
779- self.assertIsInstance(TextUIFactory()._progress_view,
780- TextProgressView)
781-
782- del os.environ['BZR_PROGRESS_BAR']
783- self.assertIsInstance(TextUIFactory()._progress_view,
784- TextProgressView)
785+ for (file_class, term, pb, expected_pb_class) in (
786+ # on an xterm, either use them or not as the user requests,
787+ # otherwise default on
788+ (_TTYStringIO, 'xterm', 'none', NullProgressView),
789+ (_TTYStringIO, 'xterm', 'text', TextProgressView),
790+ (_TTYStringIO, 'xterm', None, TextProgressView),
791+ # on a dumb terminal, again if there's explicit configuration do
792+ # it, otherwise default off
793+ (_TTYStringIO, 'dumb', 'none', NullProgressView),
794+ (_TTYStringIO, 'dumb', 'text', TextProgressView),
795+ (_TTYStringIO, 'dumb', None, NullProgressView),
796+ # on a non-tty terminal, it's null regardless of $TERM
797+ (StringIO, 'xterm', None, NullProgressView),
798+ (StringIO, 'dumb', None, NullProgressView),
799+ # however, it can still be forced on
800+ (StringIO, 'dumb', 'text', TextProgressView),
801+ ):
802+ os.environ['TERM'] = term
803+ if pb is None:
804+ if 'BZR_PROGRESS_BAR' in os.environ:
805+ del os.environ['BZR_PROGRESS_BAR']
806+ else:
807+ os.environ['BZR_PROGRESS_BAR'] = pb
808+ stdin = file_class('')
809+ stderr = file_class()
810+ stdout = file_class()
811+ uif = make_ui_for_terminal(stdin, stdout, stderr)
812+ self.assertIsInstance(uif, TextUIFactory,
813+ "TERM=%s BZR_PROGRESS_BAR=%s uif=%r" % (term, pb, uif,))
814+ self.assertIsInstance(uif.make_progress_view(),
815+ expected_pb_class,
816+ "TERM=%s BZR_PROGRESS_BAR=%s uif=%r" % (term, pb, uif,))
817+
818+ def test_text_ui_non_terminal(self):
819+ """Even on non-ttys, make_ui_for_terminal gives a text ui."""
820+ stdin = _NonTTYStringIO('')
821+ stderr = _NonTTYStringIO()
822+ stdout = _NonTTYStringIO()
823+ for term_type in ['dumb', None, 'xterm']:
824+ if term_type is None:
825+ del os.environ['TERM']
826+ else:
827+ os.environ['TERM'] = term_type
828+ uif = make_ui_for_terminal(stdin, stdout, stderr)
829+ self.assertIsInstance(uif, TextUIFactory,
830+ 'TERM=%r' % (term_type,))
831
832 def test_progress_note(self):
833 stderr = StringIO()
834@@ -137,14 +169,15 @@
835 pb.finished()
836
837 def test_progress_note_clears(self):
838- stderr = StringIO()
839- stdout = StringIO()
840- # The PQM redirects the output to a file, so it
841- # defaults to creating a Dots progress bar. we
842- # need to force it to believe we are a TTY
843+ stderr = _TTYStringIO()
844+ stdout = _TTYStringIO()
845+ # so that we get a TextProgressBar
846+ os.environ['TERM'] = 'xterm'
847 ui_factory = TextUIFactory(
848 stdin=StringIO(''),
849 stdout=stdout, stderr=stderr)
850+ self.assertIsInstance(ui_factory._progress_view,
851+ TextProgressView)
852 pb = ui_factory.nested_progress_bar()
853 try:
854 # Create a progress update that isn't throttled
855@@ -173,17 +206,17 @@
856 pb2.finished()
857 pb1.finished()
858
859- def assert_get_bool_acceptance_of_user_input(self, factory):
860- factory.stdin = StringIO("y\n" # True
861- "n\n" # False
862- "yes with garbage\nY\n" # True
863- "not an answer\nno\n" # False
864- "I'm sure!\nyes\n" # True
865- "NO\n" # False
866- "foo\n")
867- factory.stdout = StringIO()
868- factory.stderr = StringIO()
869- # there is no output from the base factory
870+ def test_text_ui_get_boolean(self):
871+ stdin = StringIO("y\n" # True
872+ "n\n" # False
873+ "yes with garbage\nY\n" # True
874+ "not an answer\nno\n" # False
875+ "I'm sure!\nyes\n" # True
876+ "NO\n" # False
877+ "foo\n")
878+ stdout = StringIO()
879+ stderr = StringIO()
880+ factory = TextUIFactory(stdin, stdout, stderr)
881 self.assertEqual(True, factory.get_boolean(""))
882 self.assertEqual(False, factory.get_boolean(""))
883 self.assertEqual(True, factory.get_boolean(""))
884@@ -194,28 +227,9 @@
885 # stdin should be empty
886 self.assertEqual('', factory.stdin.readline())
887
888- def test_silent_ui_getbool(self):
889- factory = _mod_ui.SilentUIFactory()
890- self.assert_get_bool_acceptance_of_user_input(factory)
891-
892- def test_silent_factory_prompts_silently(self):
893- factory = _mod_ui.SilentUIFactory()
894- stdout = StringIO()
895- factory.stdin = StringIO("y\n")
896- self.assertEqual(True,
897- self.apply_redirected(None, stdout, stdout,
898- factory.get_boolean, "foo"))
899- self.assertEqual("", stdout.getvalue())
900- # stdin should be empty
901- self.assertEqual('', factory.stdin.readline())
902-
903- def test_text_ui_getbool(self):
904- factory = TextUIFactory(None, None, None)
905- self.assert_get_bool_acceptance_of_user_input(factory)
906-
907 def test_text_factory_prompt(self):
908 # see <https://launchpad.net/bugs/365891>
909- factory = TextUIFactory(None, StringIO(), StringIO(), StringIO())
910+ factory = TextUIFactory(StringIO(), StringIO(), StringIO())
911 factory.prompt('foo %2e')
912 self.assertEqual('', factory.stdout.getvalue())
913 self.assertEqual('foo %2e', factory.stderr.getvalue())
914@@ -223,8 +237,8 @@
915 def test_text_factory_prompts_and_clears(self):
916 # a get_boolean call should clear the pb before prompting
917 out = _TTYStringIO()
918- factory = TextUIFactory(stdin=StringIO("yada\ny\n"),
919- stdout=out, stderr=out)
920+ os.environ['TERM'] = 'xterm'
921+ factory = TextUIFactory(stdin=StringIO("yada\ny\n"), stdout=out, stderr=out)
922 pb = factory.nested_progress_bar()
923 pb.show_bar = False
924 pb.show_spinner = False
925@@ -254,17 +268,6 @@
926 finally:
927 pb.finished()
928
929- def test_silent_ui_getusername(self):
930- factory = _mod_ui.SilentUIFactory()
931- factory.stdin = StringIO("someuser\n\n")
932- factory.stdout = StringIO()
933- factory.stderr = StringIO()
934- self.assertEquals(None,
935- factory.get_username(u'Hello\u1234 %(host)s', host=u'some\u1234'))
936- self.assertEquals("", factory.stdout.getvalue())
937- self.assertEquals("", factory.stderr.getvalue())
938- self.assertEquals("someuser\n\n", factory.stdin.getvalue())
939-
940 def test_text_ui_getusername(self):
941 factory = TextUIFactory(None, None, None)
942 factory.stdin = StringIO("someuser\n\n")
943@@ -298,63 +301,47 @@
944 pb.finished()
945
946
947-class TestTextProgressView(tests.TestCase):
948- """Tests for text display of progress bars.
949- """
950- # XXX: These might be a bit easier to write if the rendering and
951- # state-maintaining parts of TextProgressView were more separate, and if
952- # the progress task called back directly to its own view not to the ui
953- # factory. -- mbp 20090312
954+class CLIUITests(TestCase):
955+
956+ def test_cli_factory_deprecated(self):
957+ uif = self.applyDeprecated(deprecated_in((1, 18, 0)),
958+ CLIUIFactory,
959+ StringIO(), StringIO(), StringIO())
960+ self.assertIsInstance(uif, UIFactory)
961+
962+
963+class SilentUITests(TestCase):
964+
965+ def test_silent_factory_get_password(self):
966+ # A silent factory that can't do user interaction can't get a
967+ # password. Possibly it should raise a more specific error but it
968+ # can't succeed.
969+ ui = SilentUIFactory()
970+ stdout = StringIO()
971+ self.assertRaises(
972+ NotImplementedError,
973+ self.apply_redirected,
974+ None, stdout, stdout, ui.get_password)
975+ # and it didn't write anything out either
976+ self.assertEqual('', stdout.getvalue())
977+
978+ def test_silent_ui_getbool(self):
979+ factory = SilentUIFactory()
980+ stdout = StringIO()
981+ self.assertRaises(
982+ NotImplementedError,
983+ self.apply_redirected,
984+ None, stdout, stdout, factory.get_boolean, "foo")
985+
986+
987+class CannedInputUIFactoryTests(TestCase):
988
989- def _make_factory(self):
990- out = StringIO()
991- uif = TextUIFactory(stderr=out)
992- uif._progress_view._width = 80
993- return out, uif
994-
995- def test_render_progress_easy(self):
996- """Just one task and one quarter done"""
997- out, uif = self._make_factory()
998- task = uif.nested_progress_bar()
999- task.update('reticulating splines', 5, 20)
1000- self.assertEqual(
1001-'\r[####/ ] reticulating splines 5/20 \r'
1002- , out.getvalue())
1003-
1004- def test_render_progress_nested(self):
1005- """Tasks proportionally contribute to overall progress"""
1006- out, uif = self._make_factory()
1007- task = uif.nested_progress_bar()
1008- task.update('reticulating splines', 0, 2)
1009- task2 = uif.nested_progress_bar()
1010- task2.update('stage2', 1, 2)
1011- # so we're in the first half of the main task, and half way through
1012- # that
1013- self.assertEqual(
1014-r'[####\ ] reticulating splines:stage2 1/2'
1015- , uif._progress_view._render_line())
1016- # if the nested task is complete, then we're all the way through the
1017- # first half of the overall work
1018- task2.update('stage2', 2, 2)
1019- self.assertEqual(
1020-r'[#########| ] reticulating splines:stage2 2/2'
1021- , uif._progress_view._render_line())
1022-
1023- def test_render_progress_sub_nested(self):
1024- """Intermediate tasks don't mess up calculation."""
1025- out, uif = self._make_factory()
1026- task_a = uif.nested_progress_bar()
1027- task_a.update('a', 0, 2)
1028- task_b = uif.nested_progress_bar()
1029- task_b.update('b')
1030- task_c = uif.nested_progress_bar()
1031- task_c.update('c', 1, 2)
1032- # the top-level task is in its first half; the middle one has no
1033- # progress indication, just a label; and the bottom one is half done,
1034- # so the overall fraction is 1/4
1035- self.assertEqual(
1036- r'[####| ] a:b:c 1/2'
1037- , uif._progress_view._render_line())
1038+ def test_canned_input_get_input(self):
1039+ uif = CannedInputUIFactory([True, 'mbp', 'password'])
1040+ self.assertEqual(uif.get_boolean('Extra cheese?'), True)
1041+ self.assertEqual(uif.get_username('Enter your user name'), 'mbp')
1042+ self.assertEqual(uif.get_password('Password for %(host)s', host='example.com'),
1043+ 'password')
1044
1045
1046 class TestBoolFromString(tests.TestCase):
1047
1048=== modified file 'bzrlib/ui/__init__.py'
1049--- bzrlib/ui/__init__.py 2009-07-02 09:09:35 +0000
1050+++ bzrlib/ui/__init__.py 2009-07-22 08:35:20 +0000
1051@@ -14,18 +14,34 @@
1052 # along with this program; if not, write to the Free Software
1053 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1054
1055-"""UI abstraction.
1056-
1057-This tells the library how to display things to the user. Through this
1058-layer different applications can choose the style of UI.
1059-
1060-At the moment this layer is almost trivial: the application can just
1061-choose the style of progress bar.
1062-
1063-Set the ui_factory member to define the behaviour. The default
1064-displays no output.
1065+"""Abstraction for interacting with the user.
1066+
1067+Applications can choose different types of UI, and they deal with displaying
1068+messages or progress to the user, and with gathering different types of input.
1069+
1070+Several levels are supported, and you can also register new factories such as
1071+for a GUI.
1072+
1073+UIFactory
1074+ Semi-abstract base class
1075+
1076+SilentUIFactory
1077+ Produces no output and cannot take any input; useful for programs using
1078+ bzrlib in batch mode or for programs such as loggerhead.
1079+
1080+CannedInputUIFactory
1081+ For use in testing; the input values to be returned are provided
1082+ at construction.
1083+
1084+TextUIFactory
1085+ Standard text command-line interface, with stdin, stdout, stderr.
1086+ May make more or less advanced use of them, eg in drawing progress bars,
1087+ depending on the detected capabilities of the terminal.
1088+ GUIs may choose to subclass this so that unimplemented methods fall
1089+ back to working through the terminal.
1090 """
1091
1092+
1093 import os
1094 import sys
1095 import warnings
1096@@ -41,6 +57,11 @@
1097 trace,
1098 )
1099 """)
1100+from bzrlib.symbol_versioning import (
1101+ deprecated_function,
1102+ deprecated_in,
1103+ deprecated_method,
1104+ )
1105
1106
1107 _valid_boolean_strings = dict(yes=True, no=False,
1108@@ -158,6 +179,14 @@
1109 """
1110 raise NotImplementedError(self.get_boolean)
1111
1112+ def make_progress_view(self):
1113+ """Construct a new ProgressView object for this UI.
1114+
1115+ Application code should normally not call this but instead
1116+ nested_progress_bar().
1117+ """
1118+ return NullProgressView()
1119+
1120 def recommend_upgrade(self,
1121 current_format_name,
1122 basedir):
1123@@ -182,10 +211,9 @@
1124
1125
1126 class CLIUIFactory(UIFactory):
1127- """Common behaviour for command line UI factories.
1128-
1129- This is suitable for dumb terminals that can't repaint existing text."""
1130-
1131+ """Deprecated in favor of TextUIFactory."""
1132+
1133+ @deprecated_method(deprecated_in((1, 18, 0)))
1134 def __init__(self, stdin=None, stdout=None, stderr=None):
1135 UIFactory.__init__(self)
1136 self.stdin = stdin or sys.stdin
1137@@ -271,28 +299,51 @@
1138 self.stdout.write(msg + '\n')
1139
1140
1141-class SilentUIFactory(CLIUIFactory):
1142+class SilentUIFactory(UIFactory):
1143 """A UI Factory which never prints anything.
1144
1145- This is the default UI, if another one is never registered.
1146+ This is the default UI, if another one is never registered by a program
1147+ using bzrlib, and it's also active for example inside 'bzr serve'.
1148+
1149+ Methods that try to read from the user raise an error; methods that do
1150+ output do nothing.
1151 """
1152
1153 def __init__(self):
1154- CLIUIFactory.__init__(self)
1155+ UIFactory.__init__(self)
1156+
1157+ def note(self, msg):
1158+ pass
1159+
1160+ def get_username(self, prompt, **kwargs):
1161+ return None
1162+
1163+
1164+class CannedInputUIFactory(SilentUIFactory):
1165+ """A silent UI that return canned input."""
1166+
1167+ def __init__(self, responses):
1168+ self.responses = responses
1169+
1170+ def __repr__(self):
1171+ return "%s(%r)" % (self.__class__.__name__, self.responses)
1172+
1173+ def get_boolean(self, prompt):
1174+ return self.responses.pop(0)
1175
1176 def get_password(self, prompt='', **kwargs):
1177- return None
1178-
1179- def get_username(self, prompt='', **kwargs):
1180- return None
1181-
1182- def prompt(self, prompt, **kwargs):
1183- pass
1184-
1185- def note(self, msg):
1186- pass
1187-
1188-
1189+ return self.responses.pop(0)
1190+
1191+ def get_username(self, prompt, **kwargs):
1192+ return self.responses.pop(0)
1193+
1194+ def assert_all_input_consumed(self):
1195+ if self.responses:
1196+ raise AssertionError("expected all input in %r to be consumed"
1197+ % (self,))
1198+
1199+
1200+@deprecated_function(deprecated_in((1, 18, 0)))
1201 def clear_decorator(func, *args, **kwargs):
1202 """Decorator that clears the term"""
1203 ui_factory.clear_term()
1204@@ -300,28 +351,27 @@
1205
1206
1207 ui_factory = SilentUIFactory()
1208-"""IMPORTANT: never import this symbol directly. ONLY ever access it as
1209-ui.ui_factory."""
1210+# IMPORTANT: never import this symbol directly. ONLY ever access it as
1211+# ui.ui_factory, so that you refer to the current value.
1212
1213
1214 def make_ui_for_terminal(stdin, stdout, stderr):
1215 """Construct and return a suitable UIFactory for a text mode program.
1216-
1217- If stdout is a smart terminal, this gets a smart UIFactory with
1218- progress indicators, etc. If it's a dumb terminal, just plain text output.
1219 """
1220- cls = None
1221- isatty = getattr(stdin, 'isatty', None)
1222- if isatty is None:
1223- cls = CLIUIFactory
1224- elif not isatty():
1225- cls = CLIUIFactory
1226- elif os.environ.get('TERM') in ('dumb', ''):
1227- # e.g. emacs compile window
1228- cls = CLIUIFactory
1229- # User may know better, otherwise default to TextUIFactory
1230- if ( os.environ.get('BZR_USE_TEXT_UI', None) is not None
1231- or cls is None):
1232- from bzrlib.ui.text import TextUIFactory
1233- cls = TextUIFactory
1234- return cls(stdin=stdin, stdout=stdout, stderr=stderr)
1235+ # this is now always TextUIFactory, which in turn decides whether it
1236+ # should display progress bars etc
1237+ from bzrlib.ui.text import TextUIFactory
1238+ return TextUIFactory(stdin, stdout, stderr)
1239+
1240+
1241+class NullProgressView(object):
1242+ """Soak up and ignore progress information."""
1243+
1244+ def clear(self):
1245+ pass
1246+
1247+ def show_progress(self, task):
1248+ pass
1249+
1250+ def show_transport_activity(self, transport, direction, byte_count):
1251+ pass
1252
1253=== modified file 'bzrlib/ui/text.py'
1254--- bzrlib/ui/text.py 2009-06-26 03:29:57 +0000
1255+++ bzrlib/ui/text.py 2009-07-22 08:35:20 +0000
1256@@ -15,7 +15,6 @@
1257 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1258
1259
1260-
1261 """Text UI, write output to the console.
1262 """
1263
1264@@ -34,14 +33,16 @@
1265
1266 """)
1267
1268-from bzrlib.ui import CLIUIFactory
1269-
1270-
1271-class TextUIFactory(CLIUIFactory):
1272+from bzrlib.ui import (
1273+ UIFactory,
1274+ NullProgressView,
1275+ )
1276+
1277+
1278+class TextUIFactory(UIFactory):
1279 """A UI factory for Text user interefaces."""
1280
1281 def __init__(self,
1282- bar_type=None,
1283 stdin=None,
1284 stdout=None,
1285 stderr=None):
1286@@ -51,13 +52,14 @@
1287 letting the bzrlib.progress.ProgressBar factory auto
1288 select. Deprecated.
1289 """
1290- super(TextUIFactory, self).__init__(stdin=stdin,
1291- stdout=stdout, stderr=stderr)
1292- if bar_type:
1293- symbol_versioning.warn(symbol_versioning.deprecated_in((1, 11, 0))
1294- % "bar_type parameter")
1295+ super(TextUIFactory, self).__init__()
1296+ # TODO: there's no good reason not to pass all three streams, maybe we
1297+ # should deprecate the default values...
1298+ self.stdin = stdin
1299+ self.stdout = stdout
1300+ self.stderr = stderr
1301 # paints progress, network activity, etc
1302- self._progress_view = self._make_progress_view()
1303+ self._progress_view = self.make_progress_view()
1304
1305 def clear_term(self):
1306 """Prepare the terminal for output.
1307@@ -70,8 +72,78 @@
1308 # to clear it. We might need to separately check for the case of
1309 self._progress_view.clear()
1310
1311- def _make_progress_view(self):
1312- if os.environ.get('BZR_PROGRESS_BAR') in ('text', None, ''):
1313+ def get_boolean(self, prompt):
1314+ while True:
1315+ self.prompt(prompt + "? [y/n]: ")
1316+ line = self.stdin.readline().lower()
1317+ if line in ('y\n', 'yes\n'):
1318+ return True
1319+ elif line in ('n\n', 'no\n'):
1320+ return False
1321+ elif line in ('', None):
1322+ # end-of-file; possibly should raise an error here instead
1323+ return None
1324+
1325+ def get_non_echoed_password(self):
1326+ isatty = getattr(self.stdin, 'isatty', None)
1327+ if isatty is not None and isatty():
1328+ # getpass() ensure the password is not echoed and other
1329+ # cross-platform niceties
1330+ password = getpass.getpass('')
1331+ else:
1332+ # echo doesn't make sense without a terminal
1333+ password = self.stdin.readline()
1334+ if not password:
1335+ password = None
1336+ elif password[-1] == '\n':
1337+ password = password[:-1]
1338+ return password
1339+
1340+ def get_password(self, prompt='', **kwargs):
1341+ """Prompt the user for a password.
1342+
1343+ :param prompt: The prompt to present the user
1344+ :param kwargs: Arguments which will be expanded into the prompt.
1345+ This lets front ends display different things if
1346+ they so choose.
1347+ :return: The password string, return None if the user
1348+ canceled the request.
1349+ """
1350+ prompt += ': '
1351+ self.prompt(prompt, **kwargs)
1352+ # There's currently no way to say 'i decline to enter a password'
1353+ # as opposed to 'my password is empty' -- does it matter?
1354+ return self.get_non_echoed_password()
1355+
1356+ def get_username(self, prompt, **kwargs):
1357+ """Prompt the user for a username.
1358+
1359+ :param prompt: The prompt to present the user
1360+ :param kwargs: Arguments which will be expanded into the prompt.
1361+ This lets front ends display different things if
1362+ they so choose.
1363+ :return: The username string, return None if the user
1364+ canceled the request.
1365+ """
1366+ prompt += ': '
1367+ self.prompt(prompt, **kwargs)
1368+ username = self.stdin.readline()
1369+ if not username:
1370+ username = None
1371+ elif username[-1] == '\n':
1372+ username = username[:-1]
1373+ return username
1374+
1375+ def make_progress_view(self):
1376+ """Construct and return a new ProgressView subclass for this UI.
1377+ """
1378+ # if the user specifically requests either text or no progress bars,
1379+ # always do that. otherwise, guess based on $TERM and tty presence.
1380+ if os.environ.get('BZR_PROGRESS_BAR') == 'text':
1381+ return TextProgressView(self.stderr)
1382+ elif os.environ.get('BZR_PROGRESS_BAR') == 'none':
1383+ return NullProgressView()
1384+ elif progress._supports_progress(self.stderr):
1385 return TextProgressView(self.stderr)
1386 else:
1387 return NullProgressView()
1388@@ -81,6 +153,19 @@
1389 self.clear_term()
1390 self.stdout.write(msg + '\n')
1391
1392+ def prompt(self, prompt, **kwargs):
1393+ """Emit prompt on the CLI.
1394+
1395+ :param kwargs: Dictionary of arguments to insert into the prompt,
1396+ to allow UIs to reformat the prompt.
1397+ """
1398+ if kwargs:
1399+ # See <https://launchpad.net/bugs/365891>
1400+ prompt = prompt % kwargs
1401+ prompt = prompt.encode(osutils.get_terminal_encoding(), 'replace')
1402+ self.clear_term()
1403+ self.stderr.write(prompt)
1404+
1405 def report_transport_activity(self, transport, byte_count, direction):
1406 """Called by transports as they do IO.
1407
1408@@ -105,19 +190,6 @@
1409 self._progress_view.clear()
1410
1411
1412-class NullProgressView(object):
1413- """Soak up and ignore progress information."""
1414-
1415- def clear(self):
1416- pass
1417-
1418- def show_progress(self, task):
1419- pass
1420-
1421- def show_transport_activity(self, transport, direction, byte_count):
1422- pass
1423-
1424-
1425 class TextProgressView(object):
1426 """Display of progress bar and other information on a tty.
1427