Merge lp:~mbp/bzr/progress-output into lp:bzr

Proposed by Martin Pool
Status: Merged
Approved by: John A Meinel
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~mbp/bzr/progress-output
Merge into: lp:bzr
Diff against target: 340 lines (+151/-26)
9 files modified
NEWS (+6/-0)
bzrlib/builtins.py (+3/-0)
bzrlib/cmd_version_info.py (+6/-3)
bzrlib/commands.py (+4/-21)
bzrlib/log.py (+13/-2)
bzrlib/tests/per_uifactory/__init__.py (+9/-0)
bzrlib/tests/test_ui.py (+27/-0)
bzrlib/ui/__init__.py (+28/-0)
bzrlib/ui/text.py (+55/-0)
To merge this branch: bzr merge lp:~mbp/bzr/progress-output
Reviewer Review Type Date Requested Status
John A Meinel Approve
Ian Clatworthy Approve
Vincent Ladeuil Pending
Review via email: mp+14944@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :

This conceptually relates to <https://code.edge.launchpad.net/~mbp/bzr/remove-logging/+merge/14942> but doesn't technically depend on it.

+* New API ``ui_factory.make_output_stream`` to be used for sending bulk
+ (rather than user-interaction) data to stdout. This automatically
+ coordinates with progress bars or other terminal activity, and can be
+ overridden by GUIs.
+ (Martin Pool)
+

The idea is to

 * Allow us to show transport activity whenever it happens, without clobbering the output of eg log - this doesn't actually restore this behaviour, but it gets ready for it.

 * Move away from Command.outf, which assumes that it's only the command layer that knows about stdout. In fact, it's generally a layer or two down, and passing what's really a global is poor.

 * Also avoid code defaulting directly to sys.stdout.

 * In particular, put the unicode-handling features of outf into a more reusable place.

 * Give GUIs a way to capture bulk output of application-level commands to show in a window.

Command.outf still works and redirects into this.

Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (4.7 KiB)

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

Martin Pool wrote:
> Martin Pool has proposed merging lp:~mbp/bzr/progress-output into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
>
>
> This conceptually relates to <https://code.edge.launchpad.net/~mbp/bzr/remove-logging/+merge/14942> but doesn't technically depend on it.
>
> +* New API ``ui_factory.make_output_stream`` to be used for sending bulk
> + (rather than user-interaction) data to stdout. This automatically
> + coordinates with progress bars or other terminal activity, and can be
> + overridden by GUIs.
> + (Martin Pool)
> +
>
> The idea is to
>
> * Allow us to show transport activity whenever it happens, without clobbering the output of eg log - this doesn't actually restore this behaviour, but it gets ready for it.
>
> * Move away from Command.outf, which assumes that it's only the command layer that knows about stdout. In fact, it's generally a layer or two down, and passing what's really a global is poor.
>
> * Also avoid code defaulting directly to sys.stdout.
>
> * In particular, put the unicode-handling features of outf into a more reusable place.
>
> * Give GUIs a way to capture bulk output of application-level commands to show in a window.
>
> Command.outf still works and redirects into this.
>

+ def test_make_output_stream(self):
+ # at the moment this is only implemented on text uis; i'm not sure
+ # what it should do elsewhere
+ try:
+ output_stream = self.factory.make_output_stream()
+ except NotImplementedError, e:
+ raise tests.TestSkipped(str(e))
+ output_stream.write('hello!')
+

^- Since Command._setup_outf is now relying on "make_output_stream" are
they allowed to not implement it?

+class TestTextUIOutputStream(TestCase):
+ """Tests for output stream that synchronizes with progress bar."""
+
+ def test_output_clears_terminal(self):
+ stdout = StringIO()
+ stderr = StringIO()
+ clear_calls = []
+
+ uif = TextUIFactory(None, stdout, stderr)
+ uif.clear_term = lambda: clear_calls.append('clear')
+
+ stream = TextUIOutputStream(uif, uif.stdout)
+ stream.write("Hello world!\n")
+ stream.write("there's more...\n")
+ stream.writelines(["1\n", "2\n", "3\n"])
+
+ self.assertEqual(stdout.getvalue(),
+ "Hello world!\n"
+ "there's more...\n"
+ "1\n2\n3\n")
+ self.assertEqual(['clear', 'clear', 'clear'],
+ clear_calls)
+
+ stream.flush()
+
+

^- This seems like a reasonable start. Though I personally am much more
interested in what happens if you pass Unicode bytes to this stream.
Especially in the "bzr log -p" or "bzr diff" case, which tends to mix
Unicode bytes with raw content from files.

I realize you do have an 'encoding_type' flag...

+ # For whatever reason codecs.getwriter() does not advertise its
encoding
+ # it just returns the encoding of the wrapped file, which is
completely
+ # bogus. So set the attribute, so we can find the correct
encoding later.
+ out_stream = self._make_output_stream_explicit(enc...

Read more...

review: Needs Information
Revision history for this message
Gordon Tyler (doxxx) wrote :

I'd like to see this move forward since I would be making use of the updated UIFactory for my output-cleanup branch.

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

> + def test_make_output_stream(self):
> + # at the moment this is only implemented on text uis; i'm not sure
> + # what it should do elsewhere
> + try:
> + output_stream = self.factory.make_output_stream()
> + except NotImplementedError, e:
> + raise tests.TestSkipped(str(e))
> + output_stream.write('hello!')
> +
>
> ^- Since Command._setup_outf is now relying on "make_output_stream" are
> they allowed to not implement it?

It does mean that you won't be able to run Command code under any other UIFactory. I think probably it would still be reasonable to have a totally silent one that just can't run that kind of code. At least I think it's ok for now.

>
>
>
> +class TestTextUIOutputStream(TestCase):
> + """Tests for output stream that synchronizes with progress bar."""
> +
> + def test_output_clears_terminal(self):
> + stdout = StringIO()
> + stderr = StringIO()
> + clear_calls = []
> +
> + uif = TextUIFactory(None, stdout, stderr)
> + uif.clear_term = lambda: clear_calls.append('clear')
> +
> + stream = TextUIOutputStream(uif, uif.stdout)
> + stream.write("Hello world!\n")
> + stream.write("there's more...\n")
> + stream.writelines(["1\n", "2\n", "3\n"])
> +
> + self.assertEqual(stdout.getvalue(),
> + "Hello world!\n"
> + "there's more...\n"
> + "1\n2\n3\n")
> + self.assertEqual(['clear', 'clear', 'clear'],
> + clear_calls)
> +
> + stream.flush()
> +
> +
>
> ^- This seems like a reasonable start. Though I personally am much more
> interested in what happens if you pass Unicode bytes to this stream.
> Especially in the "bzr log -p" or "bzr diff" case, which tends to mix
> Unicode bytes with raw content from files.
>
> I realize you do have an 'encoding_type' flag...

At the moment the encoding type requires you to choose up front between either dealing only in bytes, or dealing only in unicode. (Or byte strings that contain only ascii can be accepted for either of course.)

I think eventually you would want to mix them as you outline below, but this is the same constraint the existing code has.

>
>
> + # For whatever reason codecs.getwriter() does not advertise its
> encoding
> + # it just returns the encoding of the wrapped file, which is
> completely
> + # bogus. So set the attribute, so we can find the correct
> encoding later.
> + out_stream = self._make_output_stream_explicit(encoding,
> encoding_type)
> + if not getattr(out_stream, 'encoding', None):
> + out_stream.encoding = encoding
> +
>
> ^- The comment doesn't fit the action. It says that "w =
> codecs.getwriter()" *does* have an 'encoding' attribute, but it returns
> the value of the wrapped object. IOW
>
> w = codecs.getwriter('utf-8', sys.stdout)
> w.encoding == sys.stdout.encoding
>
>
> rather than
> w.encoding == 'utf-8'
>
> I may be misunderstanding something, though. And if this was the code
> that was originally present, than it makes sense to preserve the behavior.

It was copied code. I might see if it can just be re...

Read more...

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

This has a test failure in a non_ascii diff test. I thought it had all passed before, but perhaps not.

Revision history for this message
John A Meinel (jameinel) wrote :

Does that mean we should be bumping it back to "Work In Progress"?

I guess my concern is that if we set it as such, then it will never get seen again...

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

This looks clean enough to merge to me. John raises a bunch of interesting questions though so please double check he's happy before landing it.

My only comment is that write() and writelines() could do with docstrings mentioning that the caller is responsible for providing newlines for each line.

review: Approve
Revision history for this message
Gordon Tyler (doxxx) wrote :

> My only comment is that write() and writelines() could do with docstrings
> mentioning that the caller is responsible for providing newlines for each
> line.

My first reaction was that this sounds like highly unintuitive API. But then I went and read up on Python's file object API and it seems it does the same thing. So I guess it's expected then.

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

@john I moved the tweak for codecs.getwriter.encoding to the place it's actually constructed and made it unconditional on the assumption the comment is correct.

The failure I'm seeing now is the same as in <https://bugs.edge.launchpad.net/bzr/+bug/328007> (or specifically the test for that) and in the comments on that bug you ask for the ability to mix 'exact' and 'replace' hunks within the output stream in the way this may eventually let you do.

So this is a bit annoying, but possibly also a chance to fix that bug more cleanly.

That bug has a somewhat hackish fix based on peeking inside EncodingWriters. I'm wondering if we should actually pass in an exact encoding file stream that it can use for the diffs, which would make things incrementally cleaner.

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

Looking at that test and bug: this is a clear case where we want to mix unicode and binary output. If you have different objects you (perhaps) need to synchronize them by flushing manually; if you have one object it may still need to flush the codec writers but it could be hidden from more callers.

If it was write_bytes() vs write_unicode() it would be more obvious in the code whether people were using the right one, vs needing to work out what kind of stream they have.

unescape_for_display is another interesting case - at the moment it needs to be told the destination encoding; it could instead become a method on the stream.

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

Quick re-review requested; here's the incremental diff

=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2009-11-04 22:32:13 +0000
+++ bzrlib/builtins.py 2009-12-08 09:06:02 +0000
@@ -2323,7 +2323,10 @@
             # Build the log formatter
             if log_format is None:
                 log_format = log.log_formatter_registry.get_default(b)
+ # Make a non-encoding output to include the diffs - bug 328007
+ unencoded_output = ui.ui_factory.make_output_stream(encoding_type='exact')
             lf = log_format(show_ids=show_ids, to_file=self.outf,
+ to_exact_file=unencoded_output,
                             show_timezone=timezone,
                             delta_format=get_verbosity_level(),
                             levels=levels,

=== modified file 'bzrlib/log.py'
--- bzrlib/log.py 2009-10-29 03:32:42 +0000
+++ bzrlib/log.py 2009-12-08 09:17:21 +0000
@@ -1291,10 +1291,13 @@
     preferred_levels = 0

     def __init__(self, to_file, show_ids=False, show_timezone='original',
- delta_format=None, levels=None, show_advice=False):
+ delta_format=None, levels=None, show_advice=False,
+ to_exact_file=None):
         """Create a LogFormatter.

         :param to_file: the file to output to
+ :param to_exact_file: if set, gives an output stream to which
+ non-Unicode diffs are written.
         :param show_ids: if True, revision-ids are to be displayed
         :param show_timezone: the timezone to use
         :param delta_format: the level of delta information to display
@@ -1307,7 +1310,13 @@
         self.to_file = to_file
         # 'exact' stream used to show diff, it should print content 'as is'
         # and should not try to decode/encode it to unicode to avoid bug #328007
- self.to_exact_file = getattr(to_file, 'stream', to_file)
+ if to_exact_file is not None:
+ self.to_exact_file = to_exact_file
+ else:
+ # XXX: somewhat hacky; this assumes it's a codec writer; it's better
+ # for code that expects to get diffs to pass in the exact file
+ # stream
+ self.to_exact_file = getattr(to_file, 'stream', to_file)
         self.show_ids = show_ids
         self.show_timezone = show_timezone
         if delta_format is None:
@@ -1463,9 +1472,11 @@
                                 short_status=False)
         if revision.diff is not None:
             to_file.write(indent + 'diff:\n')
+ to_file.flush()
             # Note: we explicitly don't indent the diff (relative to the
             # revision information) so that the output can be fed to patch -p0
             self.show_diff(self.to_exact_file, revision.diff, indent)
+ self.to_exact_file.flush()

     def get_advice_separator(self):
         """Get the text separating the log from the closing advice."""

=== modified file 'bzrlib/ui/__init__.py'
--- bzrlib/ui/__init__.py 2009-11-16 02:56:00 +0000
+++ bzrlib/ui/__init__.py 2009-12-08 08:40:11 +0000
@@ -146,12 +146,7 @@
             encoding = osutils.get_terminal_encoding()
         if encoding_type i...

Read more...

Revision history for this message
John A Meinel (jameinel) wrote :

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

Martin Pool wrote:
> Quick re-review requested; here's the incremental diff
>
> === modified file 'bzrlib/builtins.py'
> --- bzrlib/builtins.py 2009-11-04 22:32:13 +0000
> +++ bzrlib/builtins.py 2009-12-08 09:06:02 +0000
> @@ -2323,7 +2323,10 @@
> # Build the log formatter
> if log_format is None:
> log_format = log.log_formatter_registry.get_default(b)
> + # Make a non-encoding output to include the diffs - bug 328007
> + unencoded_output = ui.ui_factory.make_output_stream(encoding_type='exact')
> lf = log_format(show_ids=show_ids, to_file=self.outf,
> + to_exact_file=unencoded_output,
> show_timezone=timezone,
> delta_format=get_verbosity_level(),
> levels=levels,
>
> === modified file 'bzrlib/log.py'
> --- bzrlib/log.py 2009-10-29 03:32:42 +0000
> +++ bzrlib/log.py 2009-12-08 09:17:21 +0000
> @@ -1291,10 +1291,13 @@
> preferred_levels = 0
>
> def __init__(self, to_file, show_ids=False, show_timezone='original',
> - delta_format=None, levels=None, show_advice=False):
> + delta_format=None, levels=None, show_advice=False,
> + to_exact_file=None):
> """Create a LogFormatter.
>

Having to explicitly flush one before writing to the other is a bit
strange, and certainly the "write_bytes()" versus "write_unicode()"
makes that cleaner (IMO).

But this is a good incremental improvement over where we are at today.

 review: approve
 merge: approve

John
=:->

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

iEYEARECAAYFAkselr8ACgkQJdeBCYSNAAO8XwCePFXY9tLoAFigiZxWw4VBHipv
dlEAniXiLRPa3I6VUSCKvZVqAG68/1Fa
=VhDt
-----END PGP SIGNATURE-----

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-12-07 22:32:56 +0000
3+++ NEWS 2009-12-09 05:49:10 +0000
4@@ -241,6 +241,12 @@
5
6 * Include Japanese translations for documentation (Inada Naoki)
7
8+* New API ``ui_factory.make_output_stream`` to be used for sending bulk
9+ (rather than user-interaction) data to stdout. This automatically
10+ coordinates with progress bars or other terminal activity, and can be
11+ overridden by GUIs.
12+ (Martin Pool, 493944)
13+
14 Internals
15 *********
16
17
18=== modified file 'bzrlib/builtins.py'
19--- bzrlib/builtins.py 2009-12-08 17:30:11 +0000
20+++ bzrlib/builtins.py 2009-12-09 05:49:10 +0000
21@@ -2349,7 +2349,10 @@
22 # Build the log formatter
23 if log_format is None:
24 log_format = log.log_formatter_registry.get_default(b)
25+ # Make a non-encoding output to include the diffs - bug 328007
26+ unencoded_output = ui.ui_factory.make_output_stream(encoding_type='exact')
27 lf = log_format(show_ids=show_ids, to_file=self.outf,
28+ to_exact_file=unencoded_output,
29 show_timezone=timezone,
30 delta_format=get_verbosity_level(),
31 levels=levels,
32
33=== modified file 'bzrlib/cmd_version_info.py'
34--- bzrlib/cmd_version_info.py 2009-03-23 14:59:43 +0000
35+++ bzrlib/cmd_version_info.py 2009-12-09 05:49:10 +0000
36@@ -1,4 +1,4 @@
37-# Copyright (C) 2005, 2006 Canonical Ltd
38+# Copyright (C) 2005, 2006, 2009 Canonical Ltd
39 #
40 # This program is free software; you can redistribute it and/or modify
41 # it under the terms of the GNU General Public License as published by
42@@ -17,14 +17,17 @@
43 """Commands for generating snapshot information about a bzr tree."""
44
45 from bzrlib.lazy_import import lazy_import
46+
47 lazy_import(globals(), """
48 from bzrlib import (
49 branch,
50 errors,
51+ ui,
52+ version_info_formats,
53 workingtree,
54- version_info_formats,
55 )
56 """)
57+
58 from bzrlib.commands import Command
59 from bzrlib.option import Option, RegistryOption
60
61@@ -113,4 +116,4 @@
62 include_revision_history=include_history,
63 include_file_revisions=include_file_revisions,
64 template=template)
65- builder.generate(self.outf)
66+ builder.generate(ui.ui_factory.make_output_stream())
67
68=== modified file 'bzrlib/commands.py'
69--- bzrlib/commands.py 2009-12-02 07:56:16 +0000
70+++ bzrlib/commands.py 2009-12-09 05:49:10 +0000
71@@ -1,4 +1,4 @@
72-# Copyright (C) 2006, 2008 Canonical Ltd
73+# Copyright (C) 2006, 2008, 2009 Canonical Ltd
74 #
75 # This program is free software; you can redistribute it and/or modify
76 # it under the terms of the GNU General Public License as published by
77@@ -45,6 +45,7 @@
78 option,
79 osutils,
80 trace,
81+ ui,
82 win32utils,
83 )
84 """)
85@@ -595,26 +596,8 @@
86
87 def _setup_outf(self):
88 """Return a file linked to stdout, which has proper encoding."""
89- # Originally I was using self.stdout, but that looks
90- # *way* too much like sys.stdout
91- if self.encoding_type == 'exact':
92- # force sys.stdout to be binary stream on win32
93- if sys.platform == 'win32':
94- fileno = getattr(sys.stdout, 'fileno', None)
95- if fileno:
96- import msvcrt
97- msvcrt.setmode(fileno(), os.O_BINARY)
98- self.outf = sys.stdout
99- return
100-
101- output_encoding = osutils.get_terminal_encoding()
102-
103- self.outf = codecs.getwriter(output_encoding)(sys.stdout,
104- errors=self.encoding_type)
105- # For whatever reason codecs.getwriter() does not advertise its encoding
106- # it just returns the encoding of the wrapped file, which is completely
107- # bogus. So set the attribute, so we can find the correct encoding later.
108- self.outf.encoding = output_encoding
109+ self.outf = ui.ui_factory.make_output_stream(
110+ encoding_type=self.encoding_type)
111
112 def run_argv_aliases(self, argv, alias_argv=None):
113 """Parse the command line and run with extra aliases in alias_argv."""
114
115=== modified file 'bzrlib/log.py'
116--- bzrlib/log.py 2009-12-04 22:13:52 +0000
117+++ bzrlib/log.py 2009-12-09 05:49:10 +0000
118@@ -1293,10 +1293,13 @@
119 preferred_levels = 0
120
121 def __init__(self, to_file, show_ids=False, show_timezone='original',
122- delta_format=None, levels=None, show_advice=False):
123+ delta_format=None, levels=None, show_advice=False,
124+ to_exact_file=None):
125 """Create a LogFormatter.
126
127 :param to_file: the file to output to
128+ :param to_exact_file: if set, gives an output stream to which
129+ non-Unicode diffs are written.
130 :param show_ids: if True, revision-ids are to be displayed
131 :param show_timezone: the timezone to use
132 :param delta_format: the level of delta information to display
133@@ -1309,7 +1312,13 @@
134 self.to_file = to_file
135 # 'exact' stream used to show diff, it should print content 'as is'
136 # and should not try to decode/encode it to unicode to avoid bug #328007
137- self.to_exact_file = getattr(to_file, 'stream', to_file)
138+ if to_exact_file is not None:
139+ self.to_exact_file = to_exact_file
140+ else:
141+ # XXX: somewhat hacky; this assumes it's a codec writer; it's better
142+ # for code that expects to get diffs to pass in the exact file
143+ # stream
144+ self.to_exact_file = getattr(to_file, 'stream', to_file)
145 self.show_ids = show_ids
146 self.show_timezone = show_timezone
147 if delta_format is None:
148@@ -1494,9 +1503,11 @@
149 short_status=False)
150 if revision.diff is not None:
151 to_file.write(indent + 'diff:\n')
152+ to_file.flush()
153 # Note: we explicitly don't indent the diff (relative to the
154 # revision information) so that the output can be fed to patch -p0
155 self.show_diff(self.to_exact_file, revision.diff, indent)
156+ self.to_exact_file.flush()
157
158 def get_advice_separator(self):
159 """Get the text separating the log from the closing advice."""
160
161=== modified file 'bzrlib/tests/per_uifactory/__init__.py'
162--- bzrlib/tests/per_uifactory/__init__.py 2009-09-23 06:29:46 +0000
163+++ bzrlib/tests/per_uifactory/__init__.py 2009-12-09 05:49:10 +0000
164@@ -74,6 +74,15 @@
165 self.factory.show_warning(msg)
166 self._check_show_warning(msg)
167
168+ def test_make_output_stream(self):
169+ # at the moment this is only implemented on text uis; i'm not sure
170+ # what it should do elsewhere
171+ try:
172+ output_stream = self.factory.make_output_stream()
173+ except NotImplementedError, e:
174+ raise tests.TestSkipped(str(e))
175+ output_stream.write('hello!')
176+
177
178 class TestTextUIFactory(tests.TestCase, UIFactoryTestMixin):
179
180
181=== modified file 'bzrlib/tests/test_ui.py'
182--- bzrlib/tests/test_ui.py 2009-10-15 20:04:37 +0000
183+++ bzrlib/tests/test_ui.py 2009-12-09 05:49:10 +0000
184@@ -50,6 +50,7 @@
185 NullProgressView,
186 TextProgressView,
187 TextUIFactory,
188+ TextUIOutputStream,
189 )
190
191
192@@ -253,6 +254,32 @@
193 pb.finished()
194
195
196+class TestTextUIOutputStream(TestCase):
197+ """Tests for output stream that synchronizes with progress bar."""
198+
199+ def test_output_clears_terminal(self):
200+ stdout = StringIO()
201+ stderr = StringIO()
202+ clear_calls = []
203+
204+ uif = TextUIFactory(None, stdout, stderr)
205+ uif.clear_term = lambda: clear_calls.append('clear')
206+
207+ stream = TextUIOutputStream(uif, uif.stdout)
208+ stream.write("Hello world!\n")
209+ stream.write("there's more...\n")
210+ stream.writelines(["1\n", "2\n", "3\n"])
211+
212+ self.assertEqual(stdout.getvalue(),
213+ "Hello world!\n"
214+ "there's more...\n"
215+ "1\n2\n3\n")
216+ self.assertEqual(['clear', 'clear', 'clear'],
217+ clear_calls)
218+
219+ stream.flush()
220+
221+
222 class UITests(tests.TestCase):
223
224 def test_progress_construction(self):
225
226=== modified file 'bzrlib/ui/__init__.py'
227--- bzrlib/ui/__init__.py 2009-12-04 10:24:28 +0000
228+++ bzrlib/ui/__init__.py 2009-12-09 05:49:10 +0000
229@@ -125,6 +125,34 @@
230 """
231 raise NotImplementedError(self.get_password)
232
233+ def make_output_stream(self, encoding=None, encoding_type=None):
234+ """Get a stream for sending out bulk text data.
235+
236+ This is used for commands that produce bulk text, such as log or diff
237+ output, as opposed to user interaction. This should work even for
238+ non-interactive user interfaces. Typically this goes to a decorated
239+ version of stdout, but in a GUI it might be appropriate to send it to a
240+ window displaying the text.
241+
242+ :param encoding: Unicode encoding for output; default is the
243+ terminal encoding, which may be different from the user encoding.
244+ (See get_terminal_encoding.)
245+
246+ :param encoding_type: How to handle encoding errors:
247+ replace/strict/escape/exact. Default is replace.
248+ """
249+ # XXX: is the caller supposed to close the resulting object?
250+ if encoding is None:
251+ encoding = osutils.get_terminal_encoding()
252+ if encoding_type is None:
253+ encoding_type = 'replace'
254+ out_stream = self._make_output_stream_explicit(encoding, encoding_type)
255+ return out_stream
256+
257+ def _make_output_stream_explicit(self, encoding, encoding_type):
258+ raise NotImplementedError("%s doesn't support make_output_stream"
259+ % (self.__class__.__name__))
260+
261 def nested_progress_bar(self):
262 """Return a nested progress bar.
263
264
265=== modified file 'bzrlib/ui/text.py'
266--- bzrlib/ui/text.py 2009-12-07 10:38:09 +0000
267+++ bzrlib/ui/text.py 2009-12-09 05:49:10 +0000
268@@ -18,6 +18,7 @@
269 """Text UI, write output to the console.
270 """
271
272+import codecs
273 import getpass
274 import os
275 import sys
276@@ -146,6 +147,26 @@
277 else:
278 return NullProgressView()
279
280+ def _make_output_stream_explicit(self, encoding, encoding_type):
281+ if encoding_type == 'exact':
282+ # force sys.stdout to be binary stream on win32;
283+ # NB: this leaves the file set in that mode; may cause problems if
284+ # one process tries to do binary and then text output
285+ if sys.platform == 'win32':
286+ fileno = getattr(self.stdout, 'fileno', None)
287+ if fileno:
288+ import msvcrt
289+ msvcrt.setmode(fileno(), os.O_BINARY)
290+ return TextUIOutputStream(self, self.stdout)
291+ else:
292+ encoded_stdout = codecs.getwriter(encoding)(self.stdout,
293+ errors=encoding_type)
294+ # For whatever reason codecs.getwriter() does not advertise its encoding
295+ # it just returns the encoding of the wrapped file, which is completely
296+ # bogus. So set the attribute, so we can find the correct encoding later.
297+ encoded_stdout.encoding = encoding
298+ return TextUIOutputStream(self, encoded_stdout)
299+
300 def note(self, msg):
301 """Write an already-formatted message, clearing the progress bar if necessary."""
302 self.clear_term()
303@@ -367,3 +388,37 @@
304 self._bytes_since_update = 0
305 self._last_transport_msg = msg
306 self._repaint()
307+
308+
309+class TextUIOutputStream(object):
310+ """Decorates an output stream so that the terminal is cleared before writing.
311+
312+ This is supposed to ensure that the progress bar does not conflict with bulk
313+ text output.
314+ """
315+ # XXX: this does not handle the case of writing part of a line, then doing
316+ # progress bar output: the progress bar will probably write over it.
317+ # one option is just to buffer that text until we have a full line;
318+ # another is to save and restore it
319+
320+ # XXX: might need to wrap more methods
321+
322+ def __init__(self, ui_factory, wrapped_stream):
323+ self.ui_factory = ui_factory
324+ self.wrapped_stream = wrapped_stream
325+ # this does no transcoding, but it must expose the underlying encoding
326+ # because some callers need to know what can be written - see for
327+ # example unescape_for_display.
328+ self.encoding = getattr(wrapped_stream, 'encoding', None)
329+
330+ def flush(self):
331+ self.ui_factory.clear_term()
332+ self.wrapped_stream.flush()
333+
334+ def write(self, to_write):
335+ self.ui_factory.clear_term()
336+ self.wrapped_stream.write(to_write)
337+
338+ def writelines(self, lines):
339+ self.ui_factory.clear_term()
340+ self.wrapped_stream.writelines(lines)