/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 brzlib/ui/text.py

  • Committer: Jelmer Vernooij
  • Date: 2017-05-21 12:41:27 UTC
  • mto: This revision was merged to the branch mainline in revision 6623.
  • Revision ID: jelmer@jelmer.uk-20170521124127-iv8etg0vwymyai6y
s/bzr/brz/ in apport config.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
 
 
19
 
import codecs
20
 
import io
 
17
 
 
18
"""Text UI, write output to the console.
 
19
"""
 
20
 
 
21
from __future__ import absolute_import
 
22
 
21
23
import os
22
24
import sys
23
 
import warnings
 
25
import time
24
26
 
25
 
from ..lazy_import import lazy_import
 
27
from brzlib.lazy_import import lazy_import
26
28
lazy_import(globals(), """
 
29
import codecs
27
30
import getpass
28
 
import time
 
31
import warnings
29
32
 
30
 
from breezy import (
 
33
from brzlib import (
 
34
    config,
31
35
    debug,
32
36
    progress,
33
 
    )
34
 
""")
35
 
 
36
 
from .. import (
37
 
    config,
38
37
    osutils,
39
38
    trace,
40
39
    )
41
 
from . import (
 
40
 
 
41
""")
 
42
 
 
43
from brzlib.ui import (
 
44
    UIFactory,
42
45
    NullProgressView,
43
 
    UIFactory,
44
46
    )
45
47
 
46
48
 
57
59
    def _setup_mode(self):
58
60
        """Setup input mode (line-based, char-based) and echo-back.
59
61
 
60
 
        Line-based input is used if the BRZ_TEXTUI_INPUT environment
 
62
        Line-based input is used if the BZR_TEXTUI_INPUT environment
61
63
        variable is set to 'line-based', or if there is no controlling
62
64
        terminal.
63
65
        """
64
 
        is_tty = self.ui.raw_stdin.isatty()
65
 
        if (os.environ.get('BRZ_TEXTUI_INPUT') != 'line-based' and
66
 
                self.ui.raw_stdin == _unwrap_stream(sys.stdin) and is_tty):
 
66
        if os.environ.get('BZR_TEXTUI_INPUT') != 'line-based' and \
 
67
           self.ui.stdin == sys.stdin and self.ui.stdin.isatty():
67
68
            self.line_based = False
68
69
            self.echo_back = True
69
70
        else:
70
71
            self.line_based = True
71
 
            self.echo_back = not is_tty
 
72
            self.echo_back = not self.ui.stdin.isatty()
72
73
 
73
74
    def _build_alternatives(self, msg, choices, default):
74
75
        """Parse choices string.
119
120
 
120
121
    def _getchar(self):
121
122
        char = osutils.getchar()
122
 
        if char == chr(3):  # INTR
 
123
        if char == chr(3): # INTR
123
124
            raise KeyboardInterrupt
124
 
        if char == chr(4):  # EOF (^d, C-d)
 
125
        if char == chr(4): # EOF (^d, C-d)
125
126
            raise EOFError
126
 
        if isinstance(char, bytes):
127
 
            return char.decode('ascii', 'replace')
128
127
        return char
129
128
 
130
129
    def interact(self):
142
141
            try:
143
142
                choice = getchoice()
144
143
            except EOFError:
145
 
                self.ui.stderr.write(u'\n')
 
144
                self.ui.stderr.write('\n')
146
145
                return None
147
146
            except KeyboardInterrupt:
148
 
                self.ui.stderr.write(u'\n')
149
 
                raise
 
147
                self.ui.stderr.write('\n')
 
148
                raise KeyboardInterrupt
150
149
            choice = choice.lower()
151
150
            if choice not in self.alternatives:
152
151
                # Not a valid choice, keep on asking.
153
152
                continue
154
153
            name, index = self.alternatives[choice]
155
154
            if self.echo_back:
156
 
                self.ui.stderr.write(name + u'\n')
 
155
                self.ui.stderr.write(name + '\n')
157
156
            return index
158
157
 
159
158
 
160
159
opt_progress_bar = config.Option(
161
160
    'progress_bar', help='Progress bar type.',
162
 
    default_from_env=['BRZ_PROGRESS_BAR'], default=None,
 
161
    default_from_env=['BZR_PROGRESS_BAR'], default=None,
163
162
    invalid='error')
164
163
 
165
164
 
166
165
class TextUIFactory(UIFactory):
167
166
    """A UI factory for Text user interfaces."""
168
167
 
169
 
    def __init__(self, stdin, stdout, stderr):
170
 
        """Create a TextUIFactory."""
 
168
    def __init__(self,
 
169
                 stdin=None,
 
170
                 stdout=None,
 
171
                 stderr=None):
 
172
        """Create a TextUIFactory.
 
173
        """
171
174
        super(TextUIFactory, self).__init__()
 
175
        # TODO: there's no good reason not to pass all three streams, maybe we
 
176
        # should deprecate the default values...
172
177
        self.stdin = stdin
173
178
        self.stdout = stdout
174
179
        self.stderr = stderr
175
 
        self._progress_view = NullProgressView()
176
 
 
177
 
    def __enter__(self):
178
 
        # Choose default encoding and handle py2/3 differences
179
 
        self._setup_streams()
180
180
        # paints progress, network activity, etc
181
181
        self._progress_view = self.make_progress_view()
182
 
        return self
183
 
 
184
 
    def _setup_streams(self):
185
 
        self.raw_stdin = _unwrap_stream(self.stdin)
186
 
        self.stdin = _wrap_in_stream(self.raw_stdin)
187
 
        self.raw_stdout = _unwrap_stream(self.stdout)
188
 
        self.stdout = _wrap_out_stream(self.raw_stdout)
189
 
        self.raw_stderr = _unwrap_stream(self.stderr)
190
 
        self.stderr = _wrap_out_stream(self.raw_stderr)
191
182
 
192
183
    def choose(self, msg, choices, default=None):
193
184
        """Prompt the user for a list of alternatives.
248
239
            if not password:
249
240
                password = None
250
241
            else:
 
242
                password = password.decode(self.stdin.encoding)
 
243
 
251
244
                if password[-1] == '\n':
252
245
                    password = password[:-1]
253
246
        return password
284
277
        if not username:
285
278
            username = None
286
279
        else:
 
280
            username = username.decode(self.stdin.encoding)
287
281
            if username[-1] == '\n':
288
282
                username = username[:-1]
289
283
        return username
298
292
        if self.is_quiet():
299
293
            return NullProgressView()
300
294
        pb_type = config.GlobalStack().get('progress_bar')
301
 
        if pb_type == 'none':  # Explicit requirement
 
295
        if pb_type == 'none': # Explicit requirement
302
296
            return NullProgressView()
303
 
        if (pb_type == 'text' or # Explicit requirement
304
 
                progress._supports_progress(self.stderr)):  # Guess
 
297
        if (pb_type == 'text' # Explicit requirement
 
298
            or progress._supports_progress(self.stderr)): # Guess
305
299
            return TextProgressView(self.stderr)
306
300
        # No explicit requirement and no successful guess
307
301
        return NullProgressView()
308
302
 
309
303
    def _make_output_stream_explicit(self, encoding, encoding_type):
310
 
        return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
 
304
        if encoding_type == 'exact':
 
305
            # force sys.stdout to be binary stream on win32; 
 
306
            # NB: this leaves the file set in that mode; may cause problems if
 
307
            # one process tries to do binary and then text output
 
308
            if sys.platform == 'win32':
 
309
                fileno = getattr(self.stdout, 'fileno', None)
 
310
                if fileno:
 
311
                    import msvcrt
 
312
                    msvcrt.setmode(fileno(), os.O_BINARY)
 
313
            return TextUIOutputStream(self, self.stdout)
 
314
        else:
 
315
            encoded_stdout = codecs.getwriter(encoding)(self.stdout,
 
316
                errors=encoding_type)
 
317
            # For whatever reason codecs.getwriter() does not advertise its encoding
 
318
            # it just returns the encoding of the wrapped file, which is completely
 
319
            # bogus. So set the attribute, so we can find the correct encoding later.
 
320
            encoded_stdout.encoding = encoding
 
321
            return TextUIOutputStream(self, encoded_stdout)
311
322
 
312
323
    def note(self, msg):
313
324
        """Write an already-formatted message, clearing the progress bar if necessary."""
316
327
 
317
328
    def prompt(self, prompt, **kwargs):
318
329
        """Emit prompt on the CLI.
319
 
 
 
330
        
320
331
        :param kwargs: Dictionary of arguments to insert into the prompt,
321
332
            to allow UIs to reformat the prompt.
322
333
        """
323
 
        if not isinstance(prompt, str):
 
334
        if type(prompt) != unicode:
324
335
            raise ValueError("prompt %r not a unicode string" % prompt)
325
336
        if kwargs:
326
337
            # See <https://launchpad.net/bugs/365891>
327
338
            prompt = prompt % kwargs
 
339
        try:
 
340
            prompt = prompt.encode(self.stderr.encoding)
 
341
        except (UnicodeError, AttributeError):
 
342
            # If stderr has no encoding attribute or can't properly encode,
 
343
            # fallback to terminal encoding for robustness (better display
 
344
            # something to the user than aborting with a traceback).
 
345
            prompt = prompt.encode(osutils.get_terminal_encoding(), 'replace')
328
346
        self.clear_term()
329
347
        self.stdout.flush()
330
348
        self.stderr.write(prompt)
331
 
        self.stderr.flush()
332
349
 
333
350
    def report_transport_activity(self, transport, byte_count, direction):
334
351
        """Called by transports as they do IO.
337
354
        By default it does nothing.
338
355
        """
339
356
        self._progress_view.show_transport_activity(transport,
340
 
                                                    direction, byte_count)
 
357
            direction, byte_count)
341
358
 
342
359
    def log_transport_activity(self, display=False):
343
360
        """See UIFactory.log_transport_activity()"""
354
371
 
355
372
    def show_warning(self, msg):
356
373
        self.clear_term()
 
374
        if isinstance(msg, unicode):
 
375
            te = osutils.get_terminal_encoding()
 
376
            msg = msg.encode(te, 'replace')
357
377
        self.stderr.write("bzr: warning: %s\n" % msg)
358
378
 
359
379
    def _progress_updated(self, task):
361
381
        """
362
382
        if not self._task_stack:
363
383
            warnings.warn("%r updated but no tasks are active" %
364
 
                          (task,))
 
384
                (task,))
365
385
        elif task != self._task_stack[-1]:
366
386
            # We used to check it was the top task, but it's hard to always
367
387
            # get this right and it's not necessarily useful: any actual
368
388
            # problems will be evident in use
369
 
            # warnings.warn("%r is not the top progress task %r" %
 
389
            #warnings.warn("%r is not the top progress task %r" %
370
390
            #     (task, self._task_stack[-1]))
371
391
            pass
372
392
        self._progress_view.show_progress(task)
383
403
        # be easier to test; that has a lot of test fallout so for now just
384
404
        # new code can call this
385
405
        if warning_id not in self.suppressed_warnings:
386
 
            warning = self.format_user_warning(warning_id, message_args)
387
 
            self.stderr.write(warning + '\n')
388
 
 
389
 
 
390
 
def pad_to_width(line, width, encoding_hint='ascii'):
391
 
    """Truncate or pad unicode line to width.
392
 
 
393
 
    This is best-effort for now, and strings containing control codes or
394
 
    non-ascii text may be cut and padded incorrectly.
395
 
    """
396
 
    s = line.encode(encoding_hint, 'replace')
397
 
    return (b'%-*.*s' % (width, width, s)).decode(encoding_hint)
 
406
            self.stderr.write(self.format_user_warning(warning_id, message_args) +
 
407
                '\n')
398
408
 
399
409
 
400
410
class TextProgressView(object):
401
411
    """Display of progress bar and other information on a tty.
402
412
 
403
 
    This shows one line of text, including possibly a network indicator,
404
 
    spinner, progress bar, message, etc.
 
413
    This shows one line of text, including possibly a network indicator, spinner,
 
414
    progress bar, message, etc.
405
415
 
406
416
    One instance of this is created and held by the UI, and fed updates when a
407
417
    task wants to be painted.
412
422
    this only prints the stack from the nominated current task up to the root.
413
423
    """
414
424
 
415
 
    def __init__(self, term_file, encoding=None, errors=None):
 
425
    def __init__(self, term_file, encoding=None, errors="replace"):
416
426
        self._term_file = term_file
417
427
        if encoding is None:
418
428
            self._encoding = getattr(term_file, "encoding", None) or "ascii"
419
429
        else:
420
430
            self._encoding = encoding
 
431
        self._encoding_errors = errors
421
432
        # true when there's output on the screen we may need to clear
422
433
        self._have_output = False
423
434
        self._last_transport_msg = ''
432
443
        self._bytes_by_direction = {'unknown': 0, 'read': 0, 'write': 0}
433
444
        self._first_byte_time = None
434
445
        self._fraction = 0
435
 
        # force the progress bar to be off, as at the moment it doesn't
 
446
        # force the progress bar to be off, as at the moment it doesn't 
436
447
        # correspond reliably to overall command progress
437
448
        self.enable_bar = False
438
449
 
439
450
    def _avail_width(self):
440
451
        # we need one extra space for terminals that wrap on last char
441
 
        w = osutils.terminal_width()
 
452
        w = osutils.terminal_width() 
442
453
        if w is None:
443
454
            return None
444
455
        else:
445
456
            return w - 1
446
457
 
447
458
    def _show_line(self, u):
 
459
        s = u.encode(self._encoding, self._encoding_errors)
448
460
        width = self._avail_width()
449
461
        if width is not None:
450
 
            u = pad_to_width(u, width, encoding_hint=self._encoding)
451
 
        self._term_file.write('\r' + u + '\r')
 
462
            # GZ 2012-03-28: Counting bytes is wrong for calculating width of
 
463
            #                text but better than counting codepoints.
 
464
            s = '%-*.*s' % (width, width, s)
 
465
        self._term_file.write('\r' + s + '\r')
452
466
 
453
467
    def clear(self):
454
468
        if self._have_output:
458
472
    def _render_bar(self):
459
473
        # return a string for the progress bar itself
460
474
        if self.enable_bar and (
461
 
                (self._last_task is None) or self._last_task.show_bar):
 
475
            (self._last_task is None) or self._last_task.show_bar):
462
476
            # If there's no task object, we show space for the bar anyhow.
463
477
            # That's because most invocations of bzr will end showing progress
464
478
            # at some point, though perhaps only after doing some initial IO.
465
479
            # It looks better to draw the progress bar initially rather than
466
480
            # to have what looks like an incomplete progress bar.
467
 
            spin_str = r'/-\|'[self._spin_pos % 4]
 
481
            spin_str =  r'/-\|'[self._spin_pos % 4]
468
482
            self._spin_pos += 1
469
483
            cols = 20
470
484
            if self._last_task is None:
474
488
                completion_fraction = \
475
489
                    self._last_task._overall_completion_fraction() or 0
476
490
            if (completion_fraction < self._fraction and 'progress' in
477
 
                    debug.debug_flags):
478
 
                debug.set_trace()
 
491
                debug.debug_flags):
 
492
                import pdb;pdb.set_trace()
479
493
            self._fraction = completion_fraction
480
494
            markers = int(round(float(cols) * completion_fraction)) - 1
481
495
            bar_str = '[' + ('#' * markers + spin_str).ljust(cols) + '] '
482
496
            return bar_str
483
497
        elif (self._last_task is None) or self._last_task.show_spinner:
484
498
            # The last task wanted just a spinner, no bar
485
 
            spin_str = r'/-\|'[self._spin_pos % 4]
 
499
            spin_str =  r'/-\|'[self._spin_pos % 4]
486
500
            self._spin_pos += 1
487
501
            return spin_str + ' '
488
502
        else:
530
544
        avail_width = self._avail_width()
531
545
        if avail_width is not None:
532
546
            # if terminal avail_width is unknown, don't truncate
533
 
            current_len = len(bar_string) + len(trans) + \
534
 
                len(task_part) + len(counter_part)
535
 
            # GZ 2017-04-22: Should measure and truncate task_part properly
 
547
            current_len = len(bar_string) + len(trans) + len(task_part) + len(counter_part)
536
548
            gap = current_len - avail_width
537
549
            if gap > 0:
538
 
                task_part = task_part[:-gap - 2] + '..'
 
550
                task_part = task_part[:-gap-2] + '..'
539
551
        s = trans + bar_string + task_part + counter_part
540
552
        if avail_width is not None:
541
553
            if len(s) < avail_width:
551
563
 
552
564
    def show_progress(self, task):
553
565
        """Called by the task object when it has changed.
554
 
 
555
 
        :param task: The top task object; its parents are also included
 
566
        
 
567
        :param task: The top task object; its parents are also included 
556
568
            by following links.
557
569
        """
558
570
        must_update = task is not self._last_task
601
613
        elif now >= (self._transport_update_time + 0.5):
602
614
            # guard against clock stepping backwards, and don't update too
603
615
            # often
604
 
            rate = (self._bytes_since_update /
605
 
                    (now - self._transport_update_time))
 
616
            rate = (self._bytes_since_update
 
617
                    / (now - self._transport_update_time))
606
618
            # using base-10 units (see HACKING.txt).
607
619
            msg = ("%6dkB %5dkB/s " %
608
 
                   (self._total_byte_count / 1000, int(rate) / 1000,))
 
620
                    (self._total_byte_count / 1000, int(rate) / 1000,))
609
621
            self._transport_update_time = now
610
622
            self._last_repaint = now
611
623
            self._bytes_since_update = 0
628
640
                  bps / 1000.,
629
641
                  self._bytes_by_direction['read'] / 1000.,
630
642
                  self._bytes_by_direction['write'] / 1000.,
631
 
                  ))
 
643
                 ))
632
644
        if self._bytes_by_direction['unknown'] > 0:
633
645
            msg += ' u:%.0fkB)' % (
634
646
                self._bytes_by_direction['unknown'] / 1000.
645
657
            self._term_file.write(msg + '\n')
646
658
 
647
659
 
648
 
def _get_stream_encoding(stream):
649
 
    encoding = config.GlobalStack().get('output_encoding')
650
 
    if encoding is None:
651
 
        encoding = getattr(stream, "encoding", None)
652
 
    if encoding is None:
653
 
        encoding = osutils.get_terminal_encoding(trace=True)
654
 
    return encoding
655
 
 
656
 
 
657
 
def _unwrap_stream(stream):
658
 
    inner = getattr(stream, "buffer", None)
659
 
    if inner is None:
660
 
        inner = getattr(stream, "stream", stream)
661
 
    return inner
662
 
 
663
 
 
664
 
def _wrap_in_stream(stream, encoding=None, errors='replace'):
665
 
    if encoding is None:
666
 
        encoding = _get_stream_encoding(stream)
667
 
    # Attempt to wrap using io.open if possible, since that can do
668
 
    # line-buffering.
669
 
    try:
670
 
        fileno = stream.fileno()
671
 
    except io.UnsupportedOperation:
672
 
        encoded_stream = codecs.getreader(encoding)(stream, errors=errors)
673
 
        encoded_stream.encoding = encoding
674
 
        return encoded_stream
675
 
    else:
676
 
        return io.open(fileno, encoding=encoding, errors=errors, mode='r', buffering=1)
677
 
 
678
 
 
679
 
def _wrap_out_stream(stream, encoding=None, errors='replace'):
680
 
    if encoding is None:
681
 
        encoding = _get_stream_encoding(stream)
682
 
    encoded_stream = codecs.getwriter(encoding)(stream, errors=errors)
683
 
    encoded_stream.encoding = encoding
684
 
    return encoded_stream
685
 
 
686
 
 
687
660
class TextUIOutputStream(object):
688
 
    """Decorates stream to interact better with progress and change encoding.
689
 
 
690
 
    Before writing to the wrapped stream, progress is cleared. Callers must
691
 
    ensure bulk output is terminated with a newline so progress won't overwrite
692
 
    partial lines.
693
 
 
694
 
    Additionally, the encoding and errors behaviour of the underlying stream
695
 
    can be changed at this point. If errors is set to 'exact' raw bytes may be
696
 
    written to the underlying stream.
 
661
    """Decorates an output stream so that the terminal is cleared before writing.
 
662
 
 
663
    This is supposed to ensure that the progress bar does not conflict with bulk
 
664
    text output.
697
665
    """
698
 
 
699
 
    def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
 
666
    # XXX: this does not handle the case of writing part of a line, then doing
 
667
    # progress bar output: the progress bar will probably write over it.
 
668
    # one option is just to buffer that text until we have a full line;
 
669
    # another is to save and restore it
 
670
 
 
671
    # XXX: might need to wrap more methods
 
672
 
 
673
    def __init__(self, ui_factory, wrapped_stream):
700
674
        self.ui_factory = ui_factory
701
 
        # GZ 2017-05-21: Clean up semantics when callers are made saner.
702
 
        inner = _unwrap_stream(stream)
703
 
        self.raw_stream = None
704
 
        if errors == "exact":
705
 
            errors = "strict"
706
 
            self.raw_stream = inner
707
 
        if inner is None:
708
 
            self.wrapped_stream = stream
709
 
            if encoding is None:
710
 
                encoding = _get_stream_encoding(stream)
711
 
        else:
712
 
            self.wrapped_stream = _wrap_out_stream(inner, encoding, errors)
713
 
            if encoding is None:
714
 
                encoding = self.wrapped_stream.encoding
715
 
        self.encoding = encoding
716
 
        self.errors = errors
717
 
 
718
 
    def _write(self, to_write):
719
 
        if isinstance(to_write, bytes):
720
 
            try:
721
 
                to_write = to_write.decode(self.encoding, self.errors)
722
 
            except UnicodeDecodeError:
723
 
                self.raw_stream.write(to_write)
724
 
                return
725
 
        self.wrapped_stream.write(to_write)
 
675
        self.wrapped_stream = wrapped_stream
 
676
        # this does no transcoding, but it must expose the underlying encoding
 
677
        # because some callers need to know what can be written - see for
 
678
        # example unescape_for_display.
 
679
        self.encoding = getattr(wrapped_stream, 'encoding', None)
726
680
 
727
681
    def flush(self):
728
682
        self.ui_factory.clear_term()
730
684
 
731
685
    def write(self, to_write):
732
686
        self.ui_factory.clear_term()
733
 
        self._write(to_write)
 
687
        self.wrapped_stream.write(to_write)
734
688
 
735
689
    def writelines(self, lines):
736
690
        self.ui_factory.clear_term()
737
 
        for line in lines:
738
 
            self._write(line)
 
691
        self.wrapped_stream.writelines(lines)