/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/ui/text.py

  • Committer: Robert Collins
  • Date: 2010-05-06 11:08:10 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506110810-h3j07fh5gmw54s25
Cleaner matcher matching revised unlocking protocol.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
"""Text UI, write output to the console."""
18
17
 
19
 
from __future__ import absolute_import
 
18
"""Text UI, write output to the console.
 
19
"""
20
20
 
21
21
import codecs
 
22
import getpass
22
23
import os
23
24
import sys
 
25
import time
24
26
import warnings
25
27
 
26
 
from ..lazy_import import lazy_import
 
28
from bzrlib.lazy_import import lazy_import
27
29
lazy_import(globals(), """
28
 
import getpass
29
 
import time
30
 
 
31
 
from breezy import (
 
30
from bzrlib import (
32
31
    debug,
33
32
    progress,
34
 
    )
35
 
""")
36
 
 
37
 
from .. import (
38
 
    config,
39
33
    osutils,
 
34
    symbol_versioning,
40
35
    trace,
41
36
    )
42
 
from ..sixish import (
43
 
    text_type,
44
 
    )
45
 
from . import (
 
37
 
 
38
""")
 
39
 
 
40
from bzrlib.osutils import watch_sigwinch
 
41
 
 
42
from bzrlib.ui import (
 
43
    UIFactory,
46
44
    NullProgressView,
47
 
    UIFactory,
48
45
    )
49
46
 
50
47
 
51
 
class _ChooseUI(object):
52
 
 
53
 
    """ Helper class for choose implementation.
54
 
    """
55
 
 
56
 
    def __init__(self, ui, msg, choices, default):
57
 
        self.ui = ui
58
 
        self._setup_mode()
59
 
        self._build_alternatives(msg, choices, default)
60
 
 
61
 
    def _setup_mode(self):
62
 
        """Setup input mode (line-based, char-based) and echo-back.
63
 
 
64
 
        Line-based input is used if the BRZ_TEXTUI_INPUT environment
65
 
        variable is set to 'line-based', or if there is no controlling
66
 
        terminal.
67
 
        """
68
 
        is_tty = self.ui.raw_stdin.isatty()
69
 
        if (os.environ.get('BRZ_TEXTUI_INPUT') != 'line-based' and
70
 
                self.ui.raw_stdin == _unwrap_stream(sys.stdin) and is_tty):
71
 
            self.line_based = False
72
 
            self.echo_back = True
73
 
        else:
74
 
            self.line_based = True
75
 
            self.echo_back = not is_tty
76
 
 
77
 
    def _build_alternatives(self, msg, choices, default):
78
 
        """Parse choices string.
79
 
 
80
 
        Setup final prompt and the lists of choices and associated
81
 
        shortcuts.
82
 
        """
83
 
        index = 0
84
 
        help_list = []
85
 
        self.alternatives = {}
86
 
        choices = choices.split('\n')
87
 
        if default is not None and default not in range(0, len(choices)):
88
 
            raise ValueError("invalid default index")
89
 
        for c in choices:
90
 
            name = c.replace('&', '').lower()
91
 
            choice = (name, index)
92
 
            if name in self.alternatives:
93
 
                raise ValueError("duplicated choice: %s" % name)
94
 
            self.alternatives[name] = choice
95
 
            shortcut = c.find('&')
96
 
            if -1 != shortcut and (shortcut + 1) < len(c):
97
 
                help = c[:shortcut]
98
 
                help += '[' + c[shortcut + 1] + ']'
99
 
                help += c[(shortcut + 2):]
100
 
                shortcut = c[shortcut + 1]
101
 
            else:
102
 
                c = c.replace('&', '')
103
 
                shortcut = c[0]
104
 
                help = '[%s]%s' % (shortcut, c[1:])
105
 
            shortcut = shortcut.lower()
106
 
            if shortcut in self.alternatives:
107
 
                raise ValueError("duplicated shortcut: %s" % shortcut)
108
 
            self.alternatives[shortcut] = choice
109
 
            # Add redirections for default.
110
 
            if index == default:
111
 
                self.alternatives[''] = choice
112
 
                self.alternatives['\r'] = choice
113
 
            help_list.append(help)
114
 
            index += 1
115
 
 
116
 
        self.prompt = u'%s (%s): ' % (msg, ', '.join(help_list))
117
 
 
118
 
    def _getline(self):
119
 
        line = self.ui.stdin.readline()
120
 
        if '' == line:
121
 
            raise EOFError
122
 
        return line.strip()
123
 
 
124
 
    def _getchar(self):
125
 
        char = osutils.getchar()
126
 
        if char == chr(3):  # INTR
127
 
            raise KeyboardInterrupt
128
 
        if char == chr(4):  # EOF (^d, C-d)
129
 
            raise EOFError
130
 
        if isinstance(char, bytes):
131
 
            return char.decode('ascii', 'replace')
132
 
        return char
133
 
 
134
 
    def interact(self):
135
 
        """Keep asking the user until a valid choice is made.
136
 
        """
137
 
        if self.line_based:
138
 
            getchoice = self._getline
139
 
        else:
140
 
            getchoice = self._getchar
141
 
        iter = 0
142
 
        while True:
143
 
            iter += 1
144
 
            if 1 == iter or self.line_based:
145
 
                self.ui.prompt(self.prompt)
146
 
            try:
147
 
                choice = getchoice()
148
 
            except EOFError:
149
 
                self.ui.stderr.write(u'\n')
150
 
                return None
151
 
            except KeyboardInterrupt:
152
 
                self.ui.stderr.write(u'\n')
153
 
                raise
154
 
            choice = choice.lower()
155
 
            if choice not in self.alternatives:
156
 
                # Not a valid choice, keep on asking.
157
 
                continue
158
 
            name, index = self.alternatives[choice]
159
 
            if self.echo_back:
160
 
                self.ui.stderr.write(name + u'\n')
161
 
            return index
162
 
 
163
 
 
164
 
opt_progress_bar = config.Option(
165
 
    'progress_bar', help='Progress bar type.',
166
 
    default_from_env=['BRZ_PROGRESS_BAR'], default=None,
167
 
    invalid='error')
168
 
 
169
 
 
170
48
class TextUIFactory(UIFactory):
171
 
    """A UI factory for Text user interfaces."""
 
49
    """A UI factory for Text user interefaces."""
172
50
 
173
 
    def __init__(self, stdin, stdout, stderr):
174
 
        """Create a TextUIFactory."""
 
51
    def __init__(self,
 
52
                 stdin=None,
 
53
                 stdout=None,
 
54
                 stderr=None):
 
55
        """Create a TextUIFactory.
 
56
        """
175
57
        super(TextUIFactory, self).__init__()
 
58
        # TODO: there's no good reason not to pass all three streams, maybe we
 
59
        # should deprecate the default values...
176
60
        self.stdin = stdin
177
61
        self.stdout = stdout
178
62
        self.stderr = stderr
179
 
        self._progress_view = NullProgressView()
180
 
 
181
 
    def __enter__(self):
182
 
        # Choose default encoding and handle py2/3 differences
183
 
        self._setup_streams()
184
63
        # paints progress, network activity, etc
185
64
        self._progress_view = self.make_progress_view()
186
 
        return self
187
 
 
188
 
    def _setup_streams(self):
189
 
        self.raw_stdin = _unwrap_stream(self.stdin)
190
 
        self.stdin = _wrap_in_stream(self.raw_stdin)
191
 
        self.raw_stdout = _unwrap_stream(self.stdout)
192
 
        self.stdout = _wrap_out_stream(self.raw_stdout)
193
 
        self.raw_stderr = _unwrap_stream(self.stderr)
194
 
        self.stderr = _wrap_out_stream(self.raw_stderr)
195
 
 
196
 
    def choose(self, msg, choices, default=None):
197
 
        """Prompt the user for a list of alternatives.
198
 
 
199
 
        Support both line-based and char-based editing.
200
 
 
201
 
        In line-based mode, both the shortcut and full choice name are valid
202
 
        answers, e.g. for choose('prompt', '&yes\n&no'): 'y', ' Y ', ' yes',
203
 
        'YES ' are all valid input lines for choosing 'yes'.
204
 
 
205
 
        An empty line, when in line-based mode, or pressing enter in char-based
206
 
        mode will select the default choice (if any).
207
 
 
208
 
        Choice is echoed back if:
209
 
        - input is char-based; which means a controlling terminal is available,
210
 
          and osutils.getchar is used
211
 
        - input is line-based, and no controlling terminal is available
212
 
        """
213
 
 
214
 
        choose_ui = _ChooseUI(self, msg, choices, default)
215
 
        return choose_ui.interact()
 
65
        # hook up the signals to watch for terminal size changes
 
66
        watch_sigwinch()
216
67
 
217
68
    def be_quiet(self, state):
218
69
        if state and not self._quiet:
231
82
        # to clear it.  We might need to separately check for the case of
232
83
        self._progress_view.clear()
233
84
 
 
85
    def get_boolean(self, prompt):
 
86
        while True:
 
87
            self.prompt(prompt + "? [y/n]: ")
 
88
            line = self.stdin.readline().lower()
 
89
            if line in ('y\n', 'yes\n'):
 
90
                return True
 
91
            elif line in ('n\n', 'no\n'):
 
92
                return False
 
93
            elif line in ('', None):
 
94
                # end-of-file; possibly should raise an error here instead
 
95
                return None
 
96
 
234
97
    def get_integer(self, prompt):
235
98
        while True:
236
99
            self.prompt(prompt)
251
114
            password = self.stdin.readline()
252
115
            if not password:
253
116
                password = None
254
 
            else:
255
 
                if password[-1] == '\n':
256
 
                    password = password[:-1]
 
117
            elif password[-1] == '\n':
 
118
                password = password[:-1]
257
119
        return password
258
120
 
259
 
    def get_password(self, prompt=u'', **kwargs):
 
121
    def get_password(self, prompt='', **kwargs):
260
122
        """Prompt the user for a password.
261
123
 
262
124
        :param prompt: The prompt to present the user
287
149
        username = self.stdin.readline()
288
150
        if not username:
289
151
            username = None
290
 
        else:
291
 
            if username[-1] == '\n':
292
 
                username = username[:-1]
 
152
        elif username[-1] == '\n':
 
153
            username = username[:-1]
293
154
        return username
294
155
 
295
156
    def make_progress_view(self):
296
157
        """Construct and return a new ProgressView subclass for this UI.
297
158
        """
298
159
        # with --quiet, never any progress view
299
 
        # <https://bugs.launchpad.net/bzr/+bug/320035>.  Otherwise if the
 
160
        # <https://bugs.edge.launchpad.net/bzr/+bug/320035>.  Otherwise if the
300
161
        # user specifically requests either text or no progress bars, always
301
162
        # do that.  otherwise, guess based on $TERM and tty presence.
302
163
        if self.is_quiet():
303
164
            return NullProgressView()
304
 
        pb_type = config.GlobalStack().get('progress_bar')
305
 
        if pb_type == 'none':  # Explicit requirement
306
 
            return NullProgressView()
307
 
        if (pb_type == 'text' or # Explicit requirement
308
 
                progress._supports_progress(self.stderr)):  # Guess
309
 
            return TextProgressView(self.stderr)
310
 
        # No explicit requirement and no successful guess
311
 
        return NullProgressView()
 
165
        elif os.environ.get('BZR_PROGRESS_BAR') == 'text':
 
166
            return TextProgressView(self.stderr)
 
167
        elif os.environ.get('BZR_PROGRESS_BAR') == 'none':
 
168
            return NullProgressView()
 
169
        elif progress._supports_progress(self.stderr):
 
170
            return TextProgressView(self.stderr)
 
171
        else:
 
172
            return NullProgressView()
312
173
 
313
174
    def _make_output_stream_explicit(self, encoding, encoding_type):
314
 
        return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
 
175
        if encoding_type == 'exact':
 
176
            # force sys.stdout to be binary stream on win32; 
 
177
            # NB: this leaves the file set in that mode; may cause problems if
 
178
            # one process tries to do binary and then text output
 
179
            if sys.platform == 'win32':
 
180
                fileno = getattr(self.stdout, 'fileno', None)
 
181
                if fileno:
 
182
                    import msvcrt
 
183
                    msvcrt.setmode(fileno(), os.O_BINARY)
 
184
            return TextUIOutputStream(self, self.stdout)
 
185
        else:
 
186
            encoded_stdout = codecs.getwriter(encoding)(self.stdout,
 
187
                errors=encoding_type)
 
188
            # For whatever reason codecs.getwriter() does not advertise its encoding
 
189
            # it just returns the encoding of the wrapped file, which is completely
 
190
            # bogus. So set the attribute, so we can find the correct encoding later.
 
191
            encoded_stdout.encoding = encoding
 
192
            return TextUIOutputStream(self, encoded_stdout)
315
193
 
316
194
    def note(self, msg):
317
195
        """Write an already-formatted message, clearing the progress bar if necessary."""
320
198
 
321
199
    def prompt(self, prompt, **kwargs):
322
200
        """Emit prompt on the CLI.
323
 
 
 
201
        
324
202
        :param kwargs: Dictionary of arguments to insert into the prompt,
325
203
            to allow UIs to reformat the prompt.
326
204
        """
327
 
        if not isinstance(prompt, text_type):
328
 
            raise ValueError("prompt %r not a unicode string" % prompt)
329
205
        if kwargs:
330
206
            # See <https://launchpad.net/bugs/365891>
331
207
            prompt = prompt % kwargs
 
208
        prompt = prompt.encode(osutils.get_terminal_encoding(), 'replace')
332
209
        self.clear_term()
333
 
        self.stdout.flush()
334
210
        self.stderr.write(prompt)
335
 
        self.stderr.flush()
336
211
 
337
212
    def report_transport_activity(self, transport, byte_count, direction):
338
213
        """Called by transports as they do IO.
341
216
        By default it does nothing.
342
217
        """
343
218
        self._progress_view.show_transport_activity(transport,
344
 
                                                    direction, byte_count)
 
219
            direction, byte_count)
345
220
 
346
221
    def log_transport_activity(self, display=False):
347
222
        """See UIFactory.log_transport_activity()"""
365
240
        """
366
241
        if not self._task_stack:
367
242
            warnings.warn("%r updated but no tasks are active" %
368
 
                          (task,))
 
243
                (task,))
369
244
        elif task != self._task_stack[-1]:
370
245
            # We used to check it was the top task, but it's hard to always
371
246
            # get this right and it's not necessarily useful: any actual
372
247
            # problems will be evident in use
373
 
            # warnings.warn("%r is not the top progress task %r" %
 
248
            #warnings.warn("%r is not the top progress task %r" %
374
249
            #     (task, self._task_stack[-1]))
375
250
            pass
376
251
        self._progress_view.show_progress(task)
387
262
        # be easier to test; that has a lot of test fallout so for now just
388
263
        # new code can call this
389
264
        if warning_id not in self.suppressed_warnings:
390
 
            warning = self.format_user_warning(warning_id, message_args)
391
 
            self.stderr.write(warning + '\n')
392
 
 
393
 
 
394
 
def pad_to_width(line, width, encoding_hint='ascii'):
395
 
    """Truncate or pad unicode line to width.
396
 
 
397
 
    This is best-effort for now, and strings containing control codes or
398
 
    non-ascii text may be cut and padded incorrectly.
399
 
    """
400
 
    s = line.encode(encoding_hint, 'replace')
401
 
    return (b'%-*.*s' % (width, width, s)).decode(encoding_hint)
 
265
            self.stderr.write(self.format_user_warning(warning_id, message_args) +
 
266
                '\n')
402
267
 
403
268
 
404
269
class TextProgressView(object):
405
270
    """Display of progress bar and other information on a tty.
406
271
 
407
 
    This shows one line of text, including possibly a network indicator,
408
 
    spinner, progress bar, message, etc.
 
272
    This shows one line of text, including possibly a network indicator, spinner,
 
273
    progress bar, message, etc.
409
274
 
410
275
    One instance of this is created and held by the UI, and fed updates when a
411
276
    task wants to be painted.
416
281
    this only prints the stack from the nominated current task up to the root.
417
282
    """
418
283
 
419
 
    def __init__(self, term_file, encoding=None, errors=None):
 
284
    def __init__(self, term_file):
420
285
        self._term_file = term_file
421
 
        if encoding is None:
422
 
            self._encoding = getattr(term_file, "encoding", None) or "ascii"
423
 
        else:
424
 
            self._encoding = encoding
425
286
        # true when there's output on the screen we may need to clear
426
287
        self._have_output = False
427
288
        self._last_transport_msg = ''
436
297
        self._bytes_by_direction = {'unknown': 0, 'read': 0, 'write': 0}
437
298
        self._first_byte_time = None
438
299
        self._fraction = 0
439
 
        # force the progress bar to be off, as at the moment it doesn't
 
300
        # force the progress bar to be off, as at the moment it doesn't 
440
301
        # correspond reliably to overall command progress
441
302
        self.enable_bar = False
442
303
 
443
 
    def _avail_width(self):
444
 
        # we need one extra space for terminals that wrap on last char
445
 
        w = osutils.terminal_width()
446
 
        if w is None:
447
 
            return None
448
 
        else:
449
 
            return w - 1
450
 
 
451
 
    def _show_line(self, u):
452
 
        width = self._avail_width()
 
304
    def _show_line(self, s):
 
305
        # sys.stderr.write("progress %r\n" % s)
 
306
        width = osutils.terminal_width()
453
307
        if width is not None:
454
 
            u = pad_to_width(u, width, encoding_hint=self._encoding)
455
 
        self._term_file.write('\r' + u + '\r')
 
308
            # we need one extra space for terminals that wrap on last char
 
309
            width = width - 1
 
310
            s = '%-*.*s' % (width, width, s)
 
311
        self._term_file.write('\r' + s + '\r')
456
312
 
457
313
    def clear(self):
458
314
        if self._have_output:
462
318
    def _render_bar(self):
463
319
        # return a string for the progress bar itself
464
320
        if self.enable_bar and (
465
 
                (self._last_task is None) or self._last_task.show_bar):
 
321
            (self._last_task is None) or self._last_task.show_bar):
466
322
            # If there's no task object, we show space for the bar anyhow.
467
323
            # That's because most invocations of bzr will end showing progress
468
324
            # at some point, though perhaps only after doing some initial IO.
469
325
            # It looks better to draw the progress bar initially rather than
470
326
            # to have what looks like an incomplete progress bar.
471
 
            spin_str = r'/-\|'[self._spin_pos % 4]
 
327
            spin_str =  r'/-\|'[self._spin_pos % 4]
472
328
            self._spin_pos += 1
473
329
            cols = 20
474
330
            if self._last_task is None:
478
334
                completion_fraction = \
479
335
                    self._last_task._overall_completion_fraction() or 0
480
336
            if (completion_fraction < self._fraction and 'progress' in
481
 
                    debug.debug_flags):
482
 
                debug.set_trace()
 
337
                debug.debug_flags):
 
338
                import pdb;pdb.set_trace()
483
339
            self._fraction = completion_fraction
484
340
            markers = int(round(float(cols) * completion_fraction)) - 1
485
341
            bar_str = '[' + ('#' * markers + spin_str).ljust(cols) + '] '
486
342
            return bar_str
487
343
        elif (self._last_task is None) or self._last_task.show_spinner:
488
344
            # The last task wanted just a spinner, no bar
489
 
            spin_str = r'/-\|'[self._spin_pos % 4]
 
345
            spin_str =  r'/-\|'[self._spin_pos % 4]
490
346
            self._spin_pos += 1
491
347
            return spin_str + ' '
492
348
        else:
493
349
            return ''
494
350
 
495
351
    def _format_task(self, task):
496
 
        """Format task-specific parts of progress bar.
497
 
 
498
 
        :returns: (text_part, counter_part) both unicode strings.
499
 
        """
500
352
        if not task.show_count:
501
353
            s = ''
502
354
        elif task.current_cnt is not None and task.total_cnt is not None:
512
364
            t = t._parent_task
513
365
            if t.msg:
514
366
                m = t.msg + ':' + m
515
 
        return m, s
 
367
        return m + s
516
368
 
517
369
    def _render_line(self):
518
370
        bar_string = self._render_bar()
519
371
        if self._last_task:
520
 
            task_part, counter_part = self._format_task(self._last_task)
 
372
            task_msg = self._format_task(self._last_task)
521
373
        else:
522
 
            task_part = counter_part = ''
 
374
            task_msg = ''
523
375
        if self._last_task and not self._last_task.show_transport_activity:
524
376
            trans = ''
525
377
        else:
526
378
            trans = self._last_transport_msg
527
 
        # the bar separates the transport activity from the message, so even
528
 
        # if there's no bar or spinner, we must show something if both those
529
 
        # fields are present
530
 
        if (task_part or trans) and not bar_string:
531
 
            bar_string = '| '
532
 
        # preferentially truncate the task message if we don't have enough
533
 
        # space
534
 
        avail_width = self._avail_width()
535
 
        if avail_width is not None:
536
 
            # if terminal avail_width is unknown, don't truncate
537
 
            current_len = len(bar_string) + len(trans) + \
538
 
                len(task_part) + len(counter_part)
539
 
            # GZ 2017-04-22: Should measure and truncate task_part properly
540
 
            gap = current_len - avail_width
541
 
            if gap > 0:
542
 
                task_part = task_part[:-gap - 2] + '..'
543
 
        s = trans + bar_string + task_part + counter_part
544
 
        if avail_width is not None:
545
 
            if len(s) < avail_width:
546
 
                s = s.ljust(avail_width)
547
 
            elif len(s) > avail_width:
548
 
                s = s[:avail_width]
549
 
        return s
 
379
            if trans:
 
380
                trans += ' | '
 
381
        return (bar_string + trans + task_msg)
550
382
 
551
383
    def _repaint(self):
552
384
        s = self._render_line()
555
387
 
556
388
    def show_progress(self, task):
557
389
        """Called by the task object when it has changed.
558
 
 
559
 
        :param task: The top task object; its parents are also included
 
390
        
 
391
        :param task: The top task object; its parents are also included 
560
392
            by following links.
561
393
        """
562
394
        must_update = task is not self._last_task
605
437
        elif now >= (self._transport_update_time + 0.5):
606
438
            # guard against clock stepping backwards, and don't update too
607
439
            # often
608
 
            rate = (self._bytes_since_update /
609
 
                    (now - self._transport_update_time))
 
440
            rate = (self._bytes_since_update
 
441
                    / (now - self._transport_update_time))
610
442
            # using base-10 units (see HACKING.txt).
611
 
            msg = ("%6dkB %5dkB/s " %
612
 
                   (self._total_byte_count / 1000, int(rate) / 1000,))
 
443
            msg = ("%6dkB %5dkB/s" %
 
444
                    (self._total_byte_count / 1000, int(rate) / 1000,))
613
445
            self._transport_update_time = now
614
446
            self._last_repaint = now
615
447
            self._bytes_since_update = 0
632
464
                  bps / 1000.,
633
465
                  self._bytes_by_direction['read'] / 1000.,
634
466
                  self._bytes_by_direction['write'] / 1000.,
635
 
                  ))
 
467
                 ))
636
468
        if self._bytes_by_direction['unknown'] > 0:
637
469
            msg += ' u:%.0fkB)' % (
638
470
                self._bytes_by_direction['unknown'] / 1000.
649
481
            self._term_file.write(msg + '\n')
650
482
 
651
483
 
652
 
def _get_stream_encoding(stream):
653
 
    encoding = config.GlobalStack().get('output_encoding')
654
 
    if encoding is None:
655
 
        encoding = getattr(stream, "encoding", None)
656
 
    if encoding is None:
657
 
        encoding = osutils.get_terminal_encoding(trace=True)
658
 
    return encoding
659
 
 
660
 
 
661
 
def _unwrap_stream(stream):
662
 
    inner = getattr(stream, "buffer", None)
663
 
    if inner is None:
664
 
        inner = getattr(stream, "stream", stream)
665
 
    return inner
666
 
 
667
 
 
668
 
def _wrap_in_stream(stream, encoding=None, errors='replace'):
669
 
    if encoding is None:
670
 
        encoding = _get_stream_encoding(stream)
671
 
    encoded_stream = codecs.getreader(encoding)(stream, errors=errors)
672
 
    encoded_stream.encoding = encoding
673
 
    return encoded_stream
674
 
 
675
 
 
676
 
def _wrap_out_stream(stream, encoding=None, errors='replace'):
677
 
    if encoding is None:
678
 
        encoding = _get_stream_encoding(stream)
679
 
    encoded_stream = codecs.getwriter(encoding)(stream, errors=errors)
680
 
    encoded_stream.encoding = encoding
681
 
    return encoded_stream
682
 
 
683
 
 
684
484
class TextUIOutputStream(object):
685
 
    """Decorates stream to interact better with progress and change encoding.
686
 
 
687
 
    Before writing to the wrapped stream, progress is cleared. Callers must
688
 
    ensure bulk output is terminated with a newline so progress won't overwrite
689
 
    partial lines.
690
 
 
691
 
    Additionally, the encoding and errors behaviour of the underlying stream
692
 
    can be changed at this point. If errors is set to 'exact' raw bytes may be
693
 
    written to the underlying stream.
 
485
    """Decorates an output stream so that the terminal is cleared before writing.
 
486
 
 
487
    This is supposed to ensure that the progress bar does not conflict with bulk
 
488
    text output.
694
489
    """
695
 
 
696
 
    def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
 
490
    # XXX: this does not handle the case of writing part of a line, then doing
 
491
    # progress bar output: the progress bar will probably write over it.
 
492
    # one option is just to buffer that text until we have a full line;
 
493
    # another is to save and restore it
 
494
 
 
495
    # XXX: might need to wrap more methods
 
496
 
 
497
    def __init__(self, ui_factory, wrapped_stream):
697
498
        self.ui_factory = ui_factory
698
 
        # GZ 2017-05-21: Clean up semantics when callers are made saner.
699
 
        inner = _unwrap_stream(stream)
700
 
        self.raw_stream = None
701
 
        if errors == "exact":
702
 
            errors = "strict"
703
 
            self.raw_stream = inner
704
 
        if inner is None:
705
 
            self.wrapped_stream = stream
706
 
            if encoding is None:
707
 
                encoding = _get_stream_encoding(stream)
708
 
        else:
709
 
            self.wrapped_stream = _wrap_out_stream(inner, encoding, errors)
710
 
            if encoding is None:
711
 
                encoding = self.wrapped_stream.encoding
712
 
        self.encoding = encoding
713
 
        self.errors = errors
714
 
 
715
 
    def _write(self, to_write):
716
 
        if isinstance(to_write, bytes):
717
 
            try:
718
 
                to_write = to_write.decode(self.encoding, self.errors)
719
 
            except UnicodeDecodeError:
720
 
                self.raw_stream.write(to_write)
721
 
                return
722
 
        self.wrapped_stream.write(to_write)
 
499
        self.wrapped_stream = wrapped_stream
 
500
        # this does no transcoding, but it must expose the underlying encoding
 
501
        # because some callers need to know what can be written - see for
 
502
        # example unescape_for_display.
 
503
        self.encoding = getattr(wrapped_stream, 'encoding', None)
723
504
 
724
505
    def flush(self):
725
506
        self.ui_factory.clear_term()
727
508
 
728
509
    def write(self, to_write):
729
510
        self.ui_factory.clear_term()
730
 
        self._write(to_write)
 
511
        self.wrapped_stream.write(to_write)
731
512
 
732
513
    def writelines(self, lines):
733
514
        self.ui_factory.clear_term()
734
 
        for line in lines:
735
 
            self._write(line)
 
515
        self.wrapped_stream.writelines(lines)