Exemplo n.º 1
0
class Progress(object):
    """ Handle progress indication using callbacks.

    This class will create an object that stores information about a
    running yumsync process. It stores information about each repository
    being synced, including total packages, completed packages, and
    the status of the repository metadata. This makes it possible to
    display aggregated status of multiple repositories during a sync.
    """
    repos = {}
    totals = {
        'numpkgs': 0,
        'dlpkgs': 0,
        'md_complete': 0,
        'md_total': 0,
        'errors': 0
    }
    errors = []

    def __init__(self):
        """ records the time the sync started.
            and initialise blessings terminal """
        self.start = datetime.datetime.now()
        self.linecount = 0
        if sys.stdout.isatty():
            self.term = Terminal()
            sys.stdout.write(self.term.clear())

    def __del__(self):
        """ destructor - need to reset the terminal ."""

        if sys.stdout.isatty():
            sys.stdout.write(self.term.normal)
            sys.stdout.write(self.term.move(self.linecount, 0))
            sys.stdout.flush()

    def update(self,
               repo_id,
               set_total=None,
               pkgs_downloaded=None,
               pkg_exists=None,
               repo_metadata=None,
               repo_error=None):
        """ Handles updating the object itself.

        This method will be called any time the number of packages in
        a repository becomes known, when any package finishes downloading,
        when repository metadata begins indexing and when it completes.
        """
        if not repo_id in self.repos:
            self.repos[repo_id] = {'numpkgs': 0, 'dlpkgs': 0, 'repomd': ''}
            self.totals['md_total'] += 1
        if set_total:
            self.repos[repo_id]['numpkgs'] = set_total
            self.totals['numpkgs'] = 0
            for _, repo in six.iteritems(self.repos):
                self.totals['numpkgs'] += repo['numpkgs']
        if pkgs_downloaded:
            self.repos[repo_id]['dlpkgs'] += pkgs_downloaded
            self.totals['dlpkgs'] += pkgs_downloaded
        if repo_metadata:
            self.repos[repo_id]['repomd'] = repo_metadata
            if repo_metadata == 'complete':
                self.totals['md_complete'] += 1
        if repo_error:
            self.totals['errors'] += 1
            if self.repos[repo_id]['repomd'] != 'complete':
                self.totals['md_total'] -= 1
            self.errors.append((repo_id, repo_error))

        if sys.stdout.isatty():
            self.formatted()

    def color(self, string, color=None):
        if color and hasattr(self.term, color):
            return '{}{}{}'.format(getattr(self.term, color), string,
                                   self.term.normal)
        return string

    @classmethod
    def pct(cls, current, total):
        """ Calculate a percentage. """
        if total == 0:
            return "0"
        val = current / float(total) * 100
        formatted = '{:0.1f}%'.format(val)
        return formatted

    def elapsed(self):
        """ Calculate and return elapsed time.

        This function does dumb rounding by just plucking off anything past a
        dot "." in a time delta between two datetime.datetime()'s.
        """
        return str(datetime.datetime.now() - self.start).split('.')[0]

    def format_header(self):
        repos = self.repos.keys()
        max_repo = len(max(repos, key=len))

        repo = '{:<{}s}'.format('Repository', max_repo)
        done = '{:>5s}'.format('Done')
        total = '{:>5s}'.format('Total')
        complete = 'Packages'
        metadata = 'Metadata'
        header_str = '{}  {}/{}  {}  {}'.format(repo, done, total, complete,
                                                metadata)

        return header_str, len(repo), len(done), len(total), len(
            complete), len(metadata)

    @classmethod
    def format_line(cls, reponame, package_counts, percent, repomd):
        """ Return a string formatted for output.

        Since there is a common column layout in the progress indicator, we can
        we can implement the printf-style formatter in a function.
        """
        return '{}  {}  {}  {}'.format(reponame, package_counts, percent,
                                       repomd)

    def represent_repo_pkgs(self, repo_id, a, b):
        """ Format the ratio of packages in a repository. """
        numpkgs = self.repos[repo_id]['numpkgs']
        dlpkgs = self.repos[repo_id]['dlpkgs']
        return self.represent_pkgs(dlpkgs, numpkgs, a, b)

    def represent_total_pkgs(self, a, b):
        """ Format the total number of packages in all repositories. """
        numpkgs = self.totals['numpkgs']
        dlpkgs = self.totals['dlpkgs']
        return self.represent_pkgs(dlpkgs, numpkgs, a, b)

    @classmethod
    def represent_pkgs(cls, dlpkgs, numpkgs, a, b):
        """ Represent a package ratio.

        This will display nothing if the number of packages is 0 or unknown, or
        typical done/total if total is > 0.
        """
        if numpkgs == 0:
            return '{:^{}s}'.format('-', a + b + 1)
        else:
            return '{0:>{2}}/{1:<{3}}'.format(dlpkgs, numpkgs, a, b)

    def represent_repo_percent(self, repo_id, length):
        """ Display the percentage of packages downloaded in a repository. """
        numpkgs = self.repos[repo_id]['numpkgs']
        dlpkgs = self.repos[repo_id]['dlpkgs']
        return self.represent_percent(dlpkgs, numpkgs, length)

    def represent_total_percent(self, length):
        """ Display the overall percentage of downloaded packages. """
        numpkgs = self.totals['numpkgs']
        dlpkgs = self.totals['dlpkgs']
        return self.represent_percent(dlpkgs, numpkgs, length)

    def represent_total_metadata_percent(self, length):
        """ Display the overall percentage of metadata completion. """
        a = self.totals['md_total']
        b = self.totals['md_complete']
        return self.represent_percent(b, a, length)

    def represent_percent(self, dlpkgs, numpkgs, length):
        """ Display a percentage of completion.

        If the number of packages is unknown, nothing is displayed. Otherwise,
        a number followed by the percent sign is displayed.
        """
        if dlpkgs == 0:
            return '{:^{}s}'.format('-', length)
        else:
            return '{:^{}s}'.format(self.pct(dlpkgs, numpkgs), length)

    def represent_repomd(self, repo_id, length):
        """ Display the current status of repository metadata. """
        if not self.repos[repo_id]['repomd']:
            return '{:^{}s}'.format('-', length)
        else:
            return self.repos[repo_id]['repomd']

    def represent_repo(self, repo_id, h1, h2, h3, h4, h5):
        """ Represent an entire repository in one line.

        This makes calls to the other methods of this class to create a
        formatted string, which makes nice columns.
        """

        repo = '{:<{}s}'.format(repo_id, h1)

        if 'error' in self.repos[repo_id]:
            repo = self.color(repo, 'red')
        else:
            repo = self.color(repo, 'blue')

        packages = self.represent_repo_pkgs(repo_id, h2, h3)
        percent = self.represent_repo_percent(repo_id, h4)
        metadata = self.represent_repomd(repo_id, h5)
        if percent == 'complete':
            percent = self.color(percent, 'green')
        if metadata == 'building' or (
            (isinstance(metadata, int) or metadata.isdigit())
                and int(metadata) <= 100):
            if isinstance(metadata, str) and metadata.isdigit():
                metadata += "%"
            if isinstance(metadata, int):
                metadata = "{}%".format(metadata)
            metadata = self.color(metadata, 'yellow')
        elif metadata == 'complete':
            metadata = self.color(metadata, 'green')
        return self.format_line(repo, packages, percent, metadata)

    def represent_total(self, h1, h2, h3, h4, h5):
        total = self.color('{:>{}s}'.format('Total', h1), 'yellow')
        packages = self.represent_total_pkgs(h2, h3)
        percent = self.represent_total_percent(h4)
        metadata = self.represent_total_metadata_percent(h5)
        if percent == 'complete':
            percent = self.color(percent, 'green')
        if metadata == 'complete':
            metadata = self.color(metadata, 'green')

        return self.format_line(total, packages, percent, metadata)

    def emit(self, line=''):
        numlines = len(line.split('\n'))
        self.linecount += numlines
        with self.term.location(x=0, y=self.linecount - numlines):
            sys.stdout.write(line)
            sys.stdout.write(self.term.clear_eol())

    def formatted(self):
        """ Print all known progress data in a nicely formatted table.

        This method keeps track of what it has printed before, so that it can
        backtrack over the console screen, clearing out the previous flush and
        printing out a new one. This method is called any time any value is
        updated, which is what gives us that real-time feeling.

        Unfortunately, the YUM library calls print directly rather than just
        throwing exceptions and handling them in the presentation layer, so
        this means that yumsync's output will be slightly flawed if YUM prints
        something directly to the screen from a worker process.
        """

        # Remove repos with errors from totals
        if self.totals['errors'] > 0:
            for repo_id, error in self.errors:
                if repo_id in self.repos:
                    if not 'error' in self.repos[repo_id]:
                        self.totals['dlpkgs'] -= self.repos[repo_id]['dlpkgs']
                        self.totals['numpkgs'] -= self.repos[repo_id][
                            'numpkgs']
                        self.repos[repo_id]['error'] = True

        self.linecount = 0  # reset line counter
        header, h1, h2, h3, h4, h5 = self.format_header()
        self.emit('-' * len(header))
        self.emit(self.color('{}'.format(header), 'green'))
        self.emit('-' * len(header))

        error_repos = []
        complete_repos = []
        metadata_repos = []
        other_repos = []

        for repo_id in sorted(self.repos):
            if 'error' in self.repos[repo_id]:
                error_repos.append(repo_id)
            elif self.repos[repo_id]['repomd'] == 'complete':
                complete_repos.append(repo_id)
            elif self.repos[repo_id]['repomd']:
                metadata_repos.append(repo_id)
            else:
                other_repos.append(repo_id)

        for repo_id in sorted(self.repos):
            self.emit(self.represent_repo(repo_id, h1, h2, h3, h4, h5))

        self.emit('-' * len(header))
        self.emit(self.represent_total(h1, h2, h3, h4, h5))
        self.emit('-' * len(header))

        # Append errors to output if any found.
        if self.totals['errors'] > 0:
            self.emit(
                self.color('Errors ({}):'.format(self.totals['errors']),
                           'red'))
            for repo_id, error in self.errors:
                self.emit(self.color('{}: {}'.format(repo_id, error), 'red'))

        with self.term.location(x=0, y=self.linecount):
            sys.stdout.write(self.term.clear_eos())

        sys.stdout.flush()
Exemplo n.º 2
0
class ConsoleRender(object):
    def __init__(self, event_generator=None, theme=None, *args, **kwargs):
        super(ConsoleRender, self).__init__(*args, **kwargs)
        self._event_gen = event_generator or events.KeyEventGenerator()
        self.terminal = Terminal()
        self._previous_error = None
        self._position = 0
        self._theme = theme or themes.Default()

    def render(self, question, answers=None):
        question.answers = answers or {}

        if question.ignore:
            return question.default

        clazz = self.render_factory(question.kind)
        render = clazz(question,
                       terminal=self.terminal,
                       theme=self._theme,
                       show_default=question.show_default)

        self.clear_eos()

        try:
            return self._event_loop(render)
        finally:
            print('')

    def _event_loop(self, render):
        try:
            while True:
                self._relocate()
                self._print_status_bar(render)

                self._print_header(render)
                self._print_options(render)

                self._process_input(render)
                self._force_initial_column()
        except errors.EndOfInput as e:
            self._go_to_end(render)
            return e.selection

    def _print_status_bar(self, render):
        if self._previous_error is None:
            self.clear_bottombar()
            return

        self.render_error(self._previous_error)
        self._previous_error = None

    def _print_options(self, render):
        for message, symbol, color in render.get_options():
            if hasattr(message, 'decode'):  # python 2
                message = message.decode('utf-8')
            self.print_line(' {color}{s} {m}{t.normal}',
                            m=message,
                            color=color,
                            s=symbol)

    def _print_header(self, render):
        base = render.get_header()

        header = (base[:self.width - 9] +
                  '...' if len(base) > self.width - 6 else base)
        default_value = ' ({color}{default}{normal})'.format(
            default=render.question.default,
            color=self._theme.Question.default_color,
            normal=self.terminal.normal)
        show_default = render.question.default and render.show_default
        header += default_value if show_default else ''
        msg_template = "{t.move_up}{t.clear_eol}{tq.brackets_color}["\
                       "{tq.mark_color}?{tq.brackets_color}]{t.normal} {msg}"

        # ensure any user input with { or } will not cause a formatting error
        escaped_current_value = render.get_current_value().replace(
            '{', '{{').replace('}', '}}')
        self.print_str('\n%s: %s' % (msg_template, escaped_current_value),
                       msg=header,
                       lf=not render.title_inline,
                       tq=self._theme.Question)

    def _process_input(self, render):
        try:
            ev = self._event_gen.next()
            if isinstance(ev, events.KeyPressed):
                render.process_input(ev.value)
        except errors.EndOfInput as e:
            try:
                render.question.validate(e.selection)
                raise
            except errors.ValidationError as e:
                self._previous_error = ('"{e}" is not a valid {q}.'.format(
                    e=e.value, q=render.question.name))

    def _relocate(self):
        print(self._position * self.terminal.move_up, end='')
        self._force_initial_column()
        self._position = 0

    def _go_to_end(self, render):
        positions = len(list(render.get_options())) - self._position
        if positions > 0:
            print(self._position * self.terminal.move_down, end='')
        self._position = 0

    def _force_initial_column(self):
        self.print_str('\r')

    def render_error(self, message):
        if message:
            symbol = '>> '
            size = len(symbol) + 1
            length = len(message)
            message = message.rstrip()
            message = (message if length + size < self.width else
                       message[:self.width - (size + 3)] + '...')

            self.render_in_bottombar(
                '{t.red}{s}{t.normal}{t.bold}{msg}{t.normal} '.format(
                    msg=message, s=symbol, t=self.terminal))

    def render_in_bottombar(self, message):
        with self.terminal.location(0, self.height - 2):
            self.clear_eos()
            self.print_str(message)

    def clear_bottombar(self):
        with self.terminal.location(0, self.height - 2):
            self.clear_eos()

    def render_factory(self, question_type):
        matrix = {
            'text': Text,
            'password': Password,
            'confirm': Confirm,
            'list': List,
            'checkbox': Checkbox,
            'path': Path,
        }

        if question_type not in matrix:
            raise errors.UnknownQuestionTypeError()
        return matrix.get(question_type)

    def print_line(self, base, lf=True, **kwargs):
        self.print_str(base + self.terminal.clear_eol(), lf=lf, **kwargs)

    def print_str(self, base, lf=False, **kwargs):
        if lf:
            self._position += 1

        print(base.format(t=self.terminal, **kwargs), end='\n' if lf else '')
        sys.stdout.flush()

    def clear_eos(self):
        print(self.terminal.clear_eos(), end='')

    @property
    def width(self):
        return self.terminal.width or 80

    @property
    def height(self):
        return self.terminal.width or 24
Exemplo n.º 3
0
        elif root and cmd.split(" ")[0] == "exit":
            task = input("Do you really want to leave root? (YES/No) ")
            if task != "No" or task != "N" or task != "n" or task != "no":
                root = False
        elif not root and cmd.split(
                " ")[0] == "exit" or not root and cmd.split(" ")[0] == "quit":
            quit_task = input(
                wrap.bold(
                    "This action will cause the program to self destruct. " +
                    "(YES/No) "))
            if quit_task != "No":
                time_count = 3000
                print(wrap.clear())
                with wrap.location(0, 0):
                    print(wrap.bold("Make a wish!"))
                for i in range(time_count, 0, -1):
                    with wrap.location(0, wrap.height - 2):
                        min = int(str(i / 1000).split(".")[0])
                        mills = int(str(i / 1000).split(".")[1])
                        print(
                            wrap.bold_red(
                                'Remained %d.%d seconds before destruction. ' %
                                (min, mills)))
                    time.sleep(0.01)
                with wrap.location(0, wrap.height - 2):
                    print(wrap.clear_eos())
                with wrap.location(0, wrap.height - 2):
                    print(wrap.bold_red_blink("Time's up!"))
                    time.sleep(1)
                run = False
Exemplo n.º 4
0
class ConsoleRender(object):
    def __init__(self, event_generator=None, theme=None, *args, **kwargs):
        super(ConsoleRender, self).__init__(*args, **kwargs)
        self._event_gen = event_generator or events.KeyEventGenerator()
        self.terminal = Terminal()
        self._previous_error = None
        self._position = 0
        self._theme = theme or themes.BasicTheme()

    def render(self, question, answers=None):
        question.answers = answers or {}

        if question.ignore:
            return question.default

        clazz = self.render_factory(question.kind)
        # TODO pass theme class
        render = clazz(question, terminal=self.terminal, theme=self._theme)

        self.clear_eos()

        try:
            return self._event_loop(render)
        finally:
            print('')

    def _event_loop(self, render):
        try:
            while True:
                self._relocate()
                self._print_status_bar(render)

                self._print_header(render)
                self._print_options(render)

                self._process_input(render)
                self._force_initial_column()
        except errors.EndOfInput as e:
            self._go_to_end(render)
            return e.selection

    def _print_status_bar(self, render):
        if self._previous_error is None:
            self.clear_bottombar()
            return

        self.render_error(self._previous_error)
        self._previous_error = None

    def _print_options(self, render):
        for message, symbol, color in render.get_options():
            self.print_line(' {color}{s} {m}{t.normal}',
                            m=message,
                            color=color,
                            s=symbol)

    def _print_header(self, render):
        base = render.get_header()

        header = (base[:self.width - 9] +
                  '...' if len(base) > self.width - 6 else base)
        header += ' ({c})'.format(c=render.question.default)
        msg_template = '{t.move_up}{t.clear_eol}%(msg_prefix)s{t.normal} {msg}%(msg_postfix)s{t.normal}' % {
            'msg_prefix': self._theme.question_message_prefix,
            'msg_postfix': self._theme.question_message_postfix
        }

        self.print_str('\n%s %s' % (msg_template, render.current),
                       msg=header,
                       lf=not render.title_inline,
                       theme=self._theme)

    def _process_input(self, render):
        try:
            ev = self._event_gen.next()
            if isinstance(ev, events.KeyPressed):
                render.process_input(ev.value)
        except errors.EndOfInput as e:
            try:
                render.question.validate(e.selection)
                raise
            except errors.ValidationError as e:
                self._previous_error = ('"{e}" is not a valid {q}.'.format(
                    e=e.value, q=render.question.name))

    def _relocate(self):
        print(self._position * self.terminal.move_up, end='')
        self._force_initial_column()
        self._position = 0

    def _go_to_end(self, render):
        positions = len(list(render.get_options())) - self._position
        if positions > 0:
            print(self._position * self.terminal.move_down, end='')
        self._position = 0

    def _force_initial_column(self):
        self.print_str('\r')

    def render_error(self, message):
        if message:
            symbol = '>> '
            size = len(symbol) + 1
            length = len(message)
            message = message.rstrip()
            message = (message if length + size < self.width else
                       message[:self.width - (size + 3)] + '...')

            self.render_in_bottombar(
                '{t.red}{s}{t.normal}{t.bold}{msg}{t.normal} '.format(
                    msg=message, s=symbol, t=self.terminal))

    def render_in_bottombar(self, message):
        with self.terminal.location(0, self.height - 2):
            self.clear_eos()
            self.print_str(message)

    def clear_bottombar(self):
        with self.terminal.location(0, self.height - 2):
            self.clear_eos()

    def render_factory(self, question_type):
        matrix = {
            'text': Text,
            'password': Password,
            'confirm': Confirm,
            'list': List,
            'checkbox': Checkbox,
        }

        if question_type not in matrix:
            raise errors.UnknownQuestionTypeError()
        return matrix.get(question_type)

    def print_line(self, base, lf=True, **kwargs):
        self.print_str(base + self.terminal.clear_eol(), lf=lf, **kwargs)

    def print_str(self, base, lf=False, **kwargs):
        if lf:
            self._position += 1

        print(base.format(t=self.terminal, **kwargs), end='\n' if lf else '')
        sys.stdout.flush()

    def clear_eos(self):
        print(self.terminal.clear_eos(), end='')

    @property
    def width(self):
        return self.terminal.width or 80

    @property
    def height(self):
        return self.terminal.width or 24
Exemplo n.º 5
0
class ConsoleRender(object):
    def __init__(self, event_generator=None, *args, **kwargs):
        super(ConsoleRender, self).__init__(*args, **kwargs)
        self._event_gen = event_generator or events.KeyEventGenerator()
        self.terminal = Terminal()
        self._previous_error = None
        self._position = 0

    def render(self, question, answers=None):
        question.answers = answers or {}

        if question.ignore:
            return question.default

        clazz = self.render_factory(question.kind)
        render = clazz(question, self.terminal)

        self.clear_eos()

        try:
            return self._event_loop(render)
        finally:
            print('')

    def _event_loop(self, render):
        try:
            while True:
                self._relocate()
                self._print_status_bar(render)

                self._print_header(render)
                self._print_options(render)

                self._process_input(render)
                self._force_initial_column()
        except errors.EndOfInput as e:
            self._go_to_end(render)
            return e.selection

    def _print_status_bar(self, render):
        if self._previous_error is None:
            self.clear_bottombar()
            return

        self.render_error(self._previous_error)
        self._previous_error = None

    def _print_options(self, render):
        for message, symbol, color in render.get_options():
            self.print_line(' {color}{s} {m}{t.normal}',
                            m=message, color=color, s=symbol)

    def _print_header(self, render):
        base = render.get_header()

        header = (base[:self.width - 9] + '...'
                  if len(base) > self.width - 6
                  else base)
        header += ': {c}'.format(c=render.get_current_value())
        self.print_str(
            '\n{t.move_up}{t.clear_eol}[{t.yellow}?{t.normal}] {msg}',
            msg=header,
            lf=not render.title_inline)

    def _process_input(self, render):
        try:
            ev = self._event_gen.next()
            if isinstance(ev, events.KeyPressed):
                render.process_input(ev.value)
        except errors.EndOfInput as e:
            try:
                render.question.validate(e.selection)
                raise
            except errors.ValidationError as e:
                self._previous_error = ('"{e}" is not a valid {q}.'
                                        .format(e=e.value,
                                                q=render.question.name))

    def _relocate(self):
        print(self._position * self.terminal.move_up, end='')
        self._force_initial_column()
        self._position = 0

    def _go_to_end(self, render):
        positions = len(list(render.get_options())) - self._position
        if positions > 0:
            print(self._position * self.terminal.move_down, end='')
        self._position = 0

    def _force_initial_column(self):
        self.print_str('\r')

    def render_error(self, message):
        if message:
            symbol = '>> '
            size = len(symbol) + 1
            length = len(message)
            message = message.rstrip()
            message = (message
                       if length + size < self.width
                       else message[:self.width - (size + 3)] + '...')

            self.render_in_bottombar(
                '{t.red}{s}{t.normal}{t.bold}{msg}{t.normal} '
                .format(msg=message, s=symbol, t=self.terminal)
                )

    def render_in_bottombar(self, message):
        with self.terminal.location(0, self.height - 2):
            self.clear_eos()
            self.print_str(message)

    def clear_bottombar(self):
        with self.terminal.location(0, self.height - 2):
            self.clear_eos()

    def render_factory(self, question_type):
        matrix = {
            'text': Text,
            'password': Password,
            'confirm': Confirm,
            'list': List,
            'checkbox': Checkbox,
            }

        if question_type not in matrix:
            raise errors.UnknownQuestionTypeError()
        return matrix.get(question_type)

    def print_line(self, base, lf=True, **kwargs):
        self.print_str(base + self.terminal.clear_eol(), lf=lf, **kwargs)

    def print_str(self, base, lf=False, **kwargs):
        if lf:
            self._position += 1

        print(base.format(t=self.terminal, **kwargs), end='\n' if lf else '')
        sys.stdout.flush()

    def clear_eos(self):
        print(self.terminal.clear_eos(), end='')

    @property
    def width(self):
        return self.terminal.width or 80

    @property
    def height(self):
        return self.terminal.width or 24
Exemplo n.º 6
0
class Progress(object):
    """ Handle progress indication using callbacks.

    This class will create an object that stores information about a
    running yumsync process. It stores information about each repository
    being synced, including total packages, completed packages, and
    the status of the repository metadata. This makes it possible to
    display aggregated status of multiple repositories during a sync.
    """
    repos = {}
    totals = {
        'numpkgs': 0,
        'dlpkgs': 0,
        'md_complete': 0,
        'md_total': 0,
        'errors':0
    }
    errors = []

    def __init__(self):
      """ records the time the sync started.
          and initialise blessings terminal """
      self.start = datetime.datetime.now()
      self.linecount = 0
      if sys.stdout.isatty():
          self.term = Terminal()
          sys.stdout.write(self.term.clear())

    def __del__(self):
        """ destructor - need to reset the terminal ."""

        if sys.stdout.isatty():
            sys.stdout.write(self.term.normal)
            sys.stdout.write(self.term.move(self.linecount, 0))
            sys.stdout.flush()

    def update(self, repo_id, set_total=None, pkgs_downloaded=None,
               pkg_exists=None, repo_metadata=None, repo_error=None):
        """ Handles updating the object itself.

        This method will be called any time the number of packages in
        a repository becomes known, when any package finishes downloading,
        when repository metadata begins indexing and when it completes.
        """
        if not repo_id in self.repos:
            self.repos[repo_id] = {'numpkgs':0, 'dlpkgs':0, 'repomd':''}
            self.totals['md_total'] += 1
        if set_total:
            self.repos[repo_id]['numpkgs'] = set_total
            self.totals['numpkgs'] += set_total
        if pkgs_downloaded:
            self.repos[repo_id]['dlpkgs'] += pkgs_downloaded
            self.totals['dlpkgs'] += pkgs_downloaded
        if repo_metadata:
            self.repos[repo_id]['repomd'] = repo_metadata
            if repo_metadata == 'complete':
                self.totals['md_complete'] += 1
        if repo_error:
            self.totals['errors'] += 1
            if self.repos[repo_id]['repomd'] != 'complete':
                self.totals['md_total'] -= 1
            self.errors.append((repo_id, repo_error))

        if sys.stdout.isatty():
            self.formatted()

    def color(self, string, color=None):
        if color and hasattr(self.term, color):
            return '{}{}{}'.format(getattr(self.term, color),
                                   string,
                                   self.term.normal)
        return string

    def pct(self, current, total):
        """ Calculate a percentage. """
        val = current / float(total) * 100
        formatted = '{:0.1f}%'.format(val)
        return formatted if int(val) < 100 else 'complete'

    def elapsed(self):
        """ Calculate and return elapsed time.

        This function does dumb rounding by just plucking off anything past a
        dot "." in a time delta between two datetime.datetime()'s.
        """
        return str(datetime.datetime.now() - self.start).split('.')[0]

    def format_header(self, repos):
        max_repo = len(max(repos, key=len))

        repo = '{:<{}s}'.format('Repository', max_repo)
        done = '{:>5s}'.format('Done')
        total = '{:>5s}'.format('Total')
        complete = 'Packages'
        metadata = 'Metadata'
        header_str = '{}  {}/{}  {}  {}'.format(repo, done, total, complete, metadata)

        return header_str, len(repo), len(done), len(total), len(complete), len(metadata)

    def format_line(self, reponame, package_counts, percent, repomd):
        """ Return a string formatted for output.

        Since there is a common column layout in the progress indicator, we can
        we can implement the printf-style formatter in a function.
        """
        return '{}  {}  {}  {}'.format(reponame, package_counts, percent, repomd)

    def represent_repo_pkgs(self, repo_id, a, b):
        """ Format the ratio of packages in a repository. """
        numpkgs = self.repos[repo_id]['numpkgs']
        dlpkgs  = self.repos[repo_id]['dlpkgs']
        return self.represent_pkgs(dlpkgs, numpkgs, a, b)

    def represent_total_pkgs(self, a, b):
        """ Format the total number of packages in all repositories. """
        numpkgs = self.totals['numpkgs']
        dlpkgs  = self.totals['dlpkgs']
        return self.represent_pkgs(dlpkgs, numpkgs, a, b)

    def represent_pkgs(self, dlpkgs, numpkgs, a, b):
        """ Represent a package ratio.

        This will display nothing if the number of packages is 0 or unknown, or
        typical done/total if total is > 0.
        """
        if numpkgs == 0:
            return '{:^{}s}'.format('-', a + b + 1)
        elif dlpkgs >= numpkgs:
            return '{:>{}}'.format(dlpkgs, a + b + 1)
        else:
            return '{0:>{2}}/{1:<{3}}'.format(dlpkgs, numpkgs, a, b)


    def represent_repo_percent(self, repo_id, length):
        """ Display the percentage of packages downloaded in a repository. """
        numpkgs = self.repos[repo_id]['numpkgs']
        dlpkgs  = self.repos[repo_id]['dlpkgs']
        return self.represent_percent(dlpkgs, numpkgs, length)

    def represent_total_percent(self, length):
        """ Display the overall percentage of downloaded packages. """
        numpkgs = self.totals['numpkgs']
        dlpkgs  = self.totals['dlpkgs']
        return self.represent_percent(dlpkgs, numpkgs, length)

    def represent_total_metadata_percent(self, length):
        """ Display the overall percentage of metadata completion. """
        a = self.totals['md_total']
        b = self.totals['md_complete']
        return self.represent_percent(b, a, length)

    def represent_percent(self, dlpkgs, numpkgs, length):
        """ Display a percentage of completion.

        If the number of packages is unknown, nothing is displayed. Otherwise,
        a number followed by the percent sign is displayed.
        """
        if dlpkgs == 0:
            return '{:^{}s}'.format('-', length)
        else:
            return '{:^{}s}'.format(self.pct(dlpkgs, numpkgs), length)

    def represent_repomd(self, repo_id, length):
        """ Display the current status of repository metadata. """
        if not self.repos[repo_id]['repomd']:
            return '{:^{}s}'.format('-', length)
        else:
            return self.repos[repo_id]['repomd']

    def represent_repo(self, repo_id, h1, h2, h3, h4, h5):
        """ Represent an entire repository in one line.

        This makes calls to the other methods of this class to create a
        formatted string, which makes nice columns.
        """

        repo = '{:<{}s}'.format(repo_id, h1)

        if 'error' in self.repos[repo_id]:
            repo     = self.color(repo, 'red')
            packages = self.color('{:^{}s}'.format('error', h2 + h3 + 1), 'red')
            percent  = self.color('{:^{}s}'.format('-', h4), 'red')
            metadata = self.color('{:^{}s}'.format('-', h5), 'red')
        else:
            repo     = self.color(repo, 'blue')
            packages = self.represent_repo_pkgs(repo_id, h2, h3)
            percent  = self.represent_repo_percent(repo_id, h4)
            metadata = self.represent_repomd(repo_id, h5)
            if percent == 'complete':
                percent = self.color(percent, 'green')
            if metadata == 'building':
                metadata = self.color(metadata, 'yellow')
            elif metadata == 'complete':
                metadata = self.color(metadata, 'green')
        return self.format_line(repo, packages, percent, metadata)

    def represent_total(self, h1, h2, h3, h4, h5):
        total    = self.color('{:>{}s}'.format('Total', h1), 'yellow')
        packages = self.represent_total_pkgs(h2, h3)
        percent  = self.represent_total_percent(h4)
        metadata = self.represent_total_metadata_percent(h5)
        if percent == 'complete':
            percent = self.color(percent, 'green')
        if metadata == 'complete':
            metadata = self.color(metadata, 'green')

        return self.format_line(total, packages, percent, metadata)

    def emit(self, line=''):
        numlines = len(line.split('\n'))
        self.linecount += numlines
        with self.term.location(x=0, y=self.linecount - numlines):
            sys.stdout.write(line)
            sys.stdout.write(self.term.clear_eol())

    def formatted(self):
        """ Print all known progress data in a nicely formatted table.

        This method keeps track of what it has printed before, so that it can
        backtrack over the console screen, clearing out the previous flush and
        printing out a new one. This method is called any time any value is
        updated, which is what gives us that real-time feeling.

        Unfortunately, the YUM library calls print directly rather than just
        throwing exceptions and handling them in the presentation layer, so
        this means that yumsync's output will be slightly flawed if YUM prints
        something directly to the screen from a worker process.
        """

        # Remove repos with errors from totals
        if self.totals['errors'] > 0:
            for repo_id, error in self.errors:
                if repo_id in self.repos:
                    if not 'error' in self.repos[repo_id]:
                        self.totals['dlpkgs'] -= self.repos[repo_id]['dlpkgs']
                        self.totals['numpkgs'] -= self.repos[repo_id]['numpkgs']
                        self.repos[repo_id]['error'] = True

        self.linecount = 0  # reset line counter
        header, h1, h2, h3, h4, h5 = self.format_header(self.repos.keys())
        self.emit('-' * len(header))
        self.emit(self.color('{}'.format(header), 'green'))
        self.emit('-' * len(header))

        error_repos = []
        complete_repos = []
        metadata_repos = []
        other_repos = []

        for repo_id in sorted(self.repos):
            if 'error' in self.repos[repo_id]:
                error_repos.append(repo_id)
            elif self.repos[repo_id]['repomd'] == 'complete':
                complete_repos.append(repo_id)
            elif self.repos[repo_id]['repomd']:
                metadata_repos.append(repo_id)
            else:
                other_repos.append(repo_id)

        for repo_id in itertools.chain(error_repos, complete_repos, metadata_repos, other_repos):
            self.emit(self.represent_repo(repo_id, h1, h2, h3, h4, h5))

        self.emit('-' * len(header))
        self.emit(self.represent_total(h1, h2, h3, h4, h5))
        self.emit('-' * len(header))

        # Append errors to output if any found.
        if self.totals['errors'] > 0:
            self.emit(self.color('Errors ({}):'.format(self.totals['errors']), 'red'))
            for repo_id, error in self.errors:
                self.emit(self.color('{}: {}'.format(repo_id, error), 'red'))

        with self.term.location(x=0, y=self.linecount):
            sys.stdout.write(self.term.clear_eos())

        sys.stdout.flush()
Exemplo n.º 7
0
class ConsoleRender(object):
    def __init__(self, event_generator=None, theme=None, *args, **kwargs):
        super(ConsoleRender, self).__init__(*args, **kwargs)
        self._event_gen = event_generator or events.KeyEventGenerator()
        self.terminal = Terminal()
        self._previous_error = None
        self._position = 0
        self._theme = theme or themes.Default()

    def render(self, question, answers=None):
        question.answers = answers or {}

        if question.ignore:
            return question.default

        clazz = self.render_factory(question.kind)
        render = clazz(question,
                       terminal=self.terminal,
                       theme=self._theme,
                       show_default=question.show_default)

        self.clear_eos()

        try:
            return self._event_loop(render)
        finally:
            print('')

    def _event_loop(self, render):
        try:
            while True:
                self._relocate()
                self._print_status_bar(render)

                self._print_header(render)
                self._print_options(render)

                self._process_input(render)
                self._force_initial_column()
        except errors.EndOfInput as e:
            self._go_to_end(render)
            return e.selection

    def _print_status_bar(self, render):
        if self._previous_error is None:
            self.clear_bottombar()
            return

        self.render_error(self._previous_error)
        self._previous_error = None

    def _print_options(self, render):
        for message, symbol, color in render.get_options():
            if hasattr(message, 'decode'):  # python 2
                message = message.decode('utf-8')
            self.print_line(' {color}{s} {m}{t.normal}',
                            m=message, color=color, s=symbol)

    def _print_header(self, render):
        base = render.get_header()

        header = (base[:self.width - 9] + '...'
                  if len(base) > self.width - 6
                  else base)
        default_value = ' ({color}{default}{normal})'.format(
                             default=render.question.default,
                             color=self._theme.Question.default_color,
                             normal=self.terminal.normal)
        show_default = render.question.default and render.show_default
        header += default_value if show_default else ''
        msg_template = "{t.move_up}{t.clear_eol}{tq.brackets_color}["\
                       "{tq.mark_color}?{tq.brackets_color}]{t.normal} {msg}"

        # ensure any user input with { or } will not cause a formatting error
        escaped_current_value = (
            str(render.get_current_value())
            .replace('{', '{{')
            .replace('}', '}}')
        )
        self.print_str(
            '\n%s: %s' % (msg_template, escaped_current_value),
            msg=header,
            lf=not render.title_inline,
            tq=self._theme.Question)

    def _process_input(self, render):
        try:
            ev = self._event_gen.next()
            if isinstance(ev, events.KeyPressed):
                render.process_input(ev.value)
        except errors.ValidationError as e:
            self._previous_error = e.value
        except errors.EndOfInput as e:
            try:
                render.question.validate(e.selection)
                raise
            except errors.ValidationError as e:
                self._previous_error = render.handle_validation_error(e)

    def _relocate(self):
        print(self._position * self.terminal.move_up, end='')
        self._force_initial_column()
        self._position = 0

    def _go_to_end(self, render):
        positions = len(list(render.get_options())) - self._position
        if positions > 0:
            print(self._position * self.terminal.move_down, end='')
        self._position = 0

    def _force_initial_column(self):
        self.print_str('\r')

    def render_error(self, message):
        if message:
            symbol = '>> '
            size = len(symbol) + 1
            length = len(message)
            message = message.rstrip()
            message = (message
                       if length + size < self.width
                       else message[:self.width - (size + 3)] + '...')

            self.render_in_bottombar(
                '{t.red}{s}{t.normal}{t.bold}{msg}{t.normal} '
                .format(msg=message, s=symbol, t=self.terminal)
                )

    def render_in_bottombar(self, message):
        with self.terminal.location(0, self.height - 2):
            self.clear_eos()
            self.print_str(message)

    def clear_bottombar(self):
        with self.terminal.location(0, self.height - 2):
            self.clear_eos()

    def render_factory(self, question_type):
        matrix = {
            'text': Text,
            'editor': Editor,
            'password': Password,
            'confirm': Confirm,
            'list': List,
            'checkbox': Checkbox,
            'path': Path,
            }

        if question_type not in matrix:
            raise errors.UnknownQuestionTypeError()
        return matrix.get(question_type)

    def print_line(self, base, lf=True, **kwargs):
        self.print_str(base + self.terminal.clear_eol(), lf=lf, **kwargs)

    def print_str(self, base, lf=False, **kwargs):
        if lf:
            self._position += 1

        print(base.format(t=self.terminal, **kwargs), end='\n' if lf else '')
        sys.stdout.flush()

    def clear_eos(self):
        print(self.terminal.clear_eos(), end='')

    @property
    def width(self):
        return self.terminal.width or 80

    @property
    def height(self):
        return self.terminal.width or 24