chrismaille/buzio

View on GitHub
buzio/cli.py

Summary

Maintainability
F
6 days
Test Coverage
# -*- coding: utf-8 -*-
"""Buzio main code.

This is the main code for Buzio Package
It contains the Console class.

Return
------
    * console (obj) = Console instance
    * formatStr (obj) = Console instance with format_only=True
"""
from __future__ import print_function
import datetime
import gettext
import os
import platform
import subprocess
import sys
from colorama import Fore, Style
from unidecode import unidecode

args = {
    'domain': 'messages',
    'localedir': os.path.join('..', 'locale'),
    'fallback': True
}

t = gettext.translation(**args)
_ = t.gettext


def get_terminal_size():
    """Function: get_terminal_size.

    Try to find terminal size using get_terminal_size
    on Python 3 and 'tput' commands for Python 2.
    Limited use on Windows: just returns (80, 25)

    Return
    ------
        Tuple of Int: (col, lines)
    """
    try:
        from shutil import get_terminal_size
        return get_terminal_size(fallback=(80, 24))
    except ImportError:
        if platform.system() == "Windows":
            return (80, 25)
        cols = int(subprocess.check_output(
            'tput cols',
            shell=True,
            stderr=subprocess.STDOUT
        ))
        lines = int(subprocess.check_output(
            'tput lines',
            shell=True,
            stderr=subprocess.STDOUT
        ))
        return (cols, lines)


class Console():
    """Console class.

    Attributes:
        DEFAULT_THEMES (Dict): Default color theme
        format_only (bool): Print or format string only
        prefix (bool): Append prefix on text?
        text (str): text to be formatted/printed
        theme (str): theme selected for print/format
        theme_dict (dict): theme dictionary loaded
        transform (str): keywords for transform text
    """

    DEFAULT_THEMES = {
        'box': Fore.CYAN,
        'choose': Fore.LIGHTYELLOW_EX,
        'confirm': Fore.LIGHTMAGENTA_EX,
        'error': Fore.RED,
        'info': Fore.CYAN,
        'section': Fore.LIGHTYELLOW_EX,
        'success': Fore.GREEN,
        'warning': Fore.YELLOW,
        'dark': Fore.WHITE + Style.DIM
    }

    def __init__(self, format_only=False, default_prefix="",
                 default_transform="", default_theme="",
                 theme_dict=DEFAULT_THEMES):
        """
        Function: __init__
        Summary: InsertHere
        Examples: InsertHere

        Attributes:
            Returns: InsertHere

        Args:
            format_only (bool, optional): Description
            theme_dict (TYPE, optional): Description
        """
        self.format_only = format_only
        self.theme_dict = theme_dict
        self.transform = default_transform
        self.theme = default_theme
        self.prefix = default_prefix

    def _get_style(self):
        """Summary

        Returns:
            TYPE: Description

        Raises:
            ValueError: Description
        """
        if not isinstance(self.theme_dict, dict):
            raise ValueError("Themes List must be a dictionary.")

        return self.theme_dict.get(self.theme, "")

    def _humanize(self, obj, **kwargs):
        """Summary

        Args:
            obj (TYPE): Description

        Returns:
            TYPE: Description
        """
        if obj is None:
            ret = "--"
        elif isinstance(obj, str):
            ret = obj
        elif isinstance(obj, bool):
            ret = _("Yes") if obj else _("No")
        elif isinstance(obj,
                        (datetime.datetime, datetime.date, datetime.time)):
            date_format = kwargs.pop("date_format", False)
            if date_format:
                ret = obj.strftime(date_format)
            else:
                ret = obj.isoformat()
        elif isinstance(obj, list) or isinstance(obj, tuple):
            humanize_list = [
                self._humanize(data, **kwargs)
                for data in obj
            ]
            if self.transform and 'breakline' in self.transform:
                ret = "\n".join(humanize_list)
            else:
                ret = ", ".join(humanize_list)
        elif isinstance(obj, dict):
            if self.transform and 'show_counters' in self.transform:
                ret = "\n".join([
                    "({}) {}: {}".format(
                        i + 1,
                        key,
                        self._humanize(obj[key], **kwargs)
                    )
                    for i, key in enumerate(obj)
                ])
            else:
                ret = "\n".join([
                    "{}: {}".format(key, self._humanize(obj[key], **kwargs))
                    for key in obj
                ])
        else:
            ret = str(obj)

        return ret

    def _print(self, linebreak=False):
        """Summary

        Parameters
        ----------
        linebreak : bool, optional
            Description

        Returns
        -------
        TYPE
            Description
        """
        if self.prefix:
            if self.transform and 'breakline' in self.transform:
                self.text = "{}:\n{}".format(self.prefix, self.text)
            else:
                self.text = "{}: {}".format(self.prefix, self.text)

        if self.transform:
            if 'title' in self.transform:
                self.text = self.text.title()
            if 'upper' in self.transform:
                self.text = self.text.upper()
            if 'small' in self.transform:
                self.text = self.text.lower()

        if self.theme:
            self.text = [
                "{}{}{}{}".format(
                    Style.BRIGHT
                    if self.transform and 'bold' in self.transform else "",
                    self._get_style(),
                    line,
                    Style.RESET_ALL
                )
                for line in self.text.split("\n")
            ]

        if not self.format_only:
            print("\n".join(self.text), end="\n" if linebreak else "" + "\n")

        string = "{}".format("\n" if linebreak else "")
        return format(string.join(self.text))

    def _run_style(self, obj, theme, transform,
                   use_prefix, prefix, humanize, **kwargs):
        self.transform = transform
        self.text = self._humanize(obj, **kwargs) if humanize else obj
        self.prefix = prefix if use_prefix else ""
        self.theme = theme
        return self._print()

    def success(
            self,
            obj,
            theme="success",
            transform=None,
            use_prefix=True,
            prefix="Success",
            humanize=True,
            **kwargs):
        """
        Args:
            obj (TYPE): Description
            theme (str, optional): Description
            transform (None, optional): Description
            use_prefix (bool, optional): Description
            prefix (str, optional): Description
            humanize (bool, optional): Description

        Returns:
            TYPE: Description
        """
        return self._run_style(obj, theme, transform,
                               use_prefix, prefix, humanize, **kwargs)

    def info(
            self,
            obj,
            theme="info",
            transform=None,
            use_prefix=True,
            prefix="Info",
            humanize=True,
            **kwargs):
        """
        Args:
            obj (TYPE): Description
            theme (str, optional): Description
            transform (None, optional): Description
            use_prefix (bool, optional): Description
            prefix (str, optional): Description
            humanize (bool, optional): Description

        Returns:
            TYPE: Description
        """
        return self._run_style(obj, theme, transform,
                               use_prefix, prefix, humanize, **kwargs)

    def warning(
            self,
            obj,
            theme="warning",
            transform=None,
            use_prefix=True,
            prefix="Warning",
            humanize=True,
            **kwargs):
        """
        Args:
            obj (TYPE): Description
            theme (str, optional): Description
            transform (None, optional): Description
            use_prefix (bool, optional): Description
            prefix (str, optional): Description
            humanize (bool, optional): Description

        Returns:
            TYPE: Description
        """
        return self._run_style(obj, theme, transform,
                               use_prefix, prefix, humanize, **kwargs)

    def error(
            self,
            obj,
            theme="error",
            transform=None,
            use_prefix=True,
            prefix="Error",
            humanize=True,
            **kwargs):
        """
        Args:
            obj (TYPE): Description
            theme (str, optional): Description
            transform (None, optional): Description
            use_prefix (bool, optional): Description
            prefix (str, optional): Description
            humanize (bool, optional): Description

        Returns:
            TYPE: Description
        """
        return self._run_style(obj, theme, transform,
                               use_prefix, prefix, humanize, **kwargs)

    def section(
            self,
            obj,
            theme="section",
            transform=None,
            use_prefix=False,
            prefix="Section",
            full_width=False,
            humanize=True,
            **kwargs):
        """
        Args:
            obj (TYPE): Description
            theme (str, optional): Description
            transform (None, optional): Description
            use_prefix (bool, optional): Description
            prefix (str, optional): Description
            full_width (bool, optional): Description
            humanize (bool, optional): Description

        Returns:
            TYPE: Description
        """
        self.transform = transform
        self.text = self._humanize(obj, **kwargs) if humanize else obj
        if transform and 'center' in transform:
            format_text = "> {:^{num}} <"
            extra_chars = 4
        elif transform and 'right' in transform:
            format_text = "{:>{num}} <<"
            extra_chars = 3
        else:
            format_text = ">> {:<{num}}"
            extra_chars = 3
        if full_width:
            longest_line = get_terminal_size()[0]
        else:
            line_sizes = [
                len(line) + extra_chars
                for line in self.text.split("\n")
            ]
            longest_line = sorted(line_sizes, reverse=True)[0]
        main_lines = [
            format_text.format(line, num=longest_line - 4)
            for line in self.text.split("\n")
        ]
        bottom_line = "{:-^{num}}".format('', num=longest_line)
        self.text = "\n{}\n{}\n".format(
            "\n".join(main_lines),
            bottom_line
        )
        self.prefix = prefix if use_prefix else ""
        self.theme = theme
        return self._print()

    def box(self, obj, theme="box", transform=None, humanize=True, **kwargs):
        """
        Function: box
        Summary: InsertHere
        Examples: InsertHere

        Attributes:
            Returns: InsertHere

        Args:
            obj (TYPE): Description
            theme (str, optional): Description
            transform (None, optional): Description
            humanize (bool, optional): Description

        Returns:
            TYPE: Description
        """
        self.transform = transform
        self.text = self._humanize(obj, **kwargs) if humanize else obj
        line_sizes = [
            len(line)
            for line in self.text.split("\n")
        ]
        longest_line = sorted(line_sizes, reverse=True)[0]

        horizontal_line = "{:*^{num}}".format('', num=longest_line + 6)
        vertical_line = "*{:^{num}}*".format('', num=longest_line + 4)

        main_texts = [
            "*{:^{num}}*".format(
                text_line, num=longest_line + 4
            )
            for text_line in self.text.split("\n")
        ]
        self.text = "{}\n{}\n{}\n{}\n{}".format(
            horizontal_line,
            vertical_line,
            "\n".join(main_texts),
            vertical_line,
            horizontal_line
        )
        self.theme = theme
        self.prefix = None
        return self._print()

    def confirm(
            self,
            obj=None,
            theme="confirm",
            transform=None,
            humanize=True,
            default=None,
            **kwargs):
        """
        Args:
            obj (None, optional): Description
            theme (str, optional): Description
            transform (None, optional): Description
            humanize (bool, optional): Description
            default (None, optional): Description

        Returns:
            TYPE: Description

        Raises:
            ValueError: Description
        """
        if default is not None and not isinstance(default, bool):
            raise ValueError("Default must be a boolean")

        self.transform = transform
        if obj:
            self.text = self._humanize(obj, **kwargs) if humanize else obj
        else:
            self.text = _("Please confirm")
        answered = False
        self.text = "{} {}{} ".format(
            self.text,
            _("(y/n)"),
            "[{}]".format(self._humanize(default, **kwargs)[0])
            if default is not None else "")
        self.prefix = None
        self.theme = theme
        self._print(linebreak=False)
        if self.format_only:
            return self.text
        while not answered:
            ret = input(_("? "))
            if not ret and default is not None:
                ret = default
                answered = True
            elif ret and ret[0].upper() in \
                    [_("Yes")[0].upper(), _("No")[0].upper()]:
                answered = True
        return ret if isinstance(
            ret, bool) else ret[0].upper() == _("Yes")[0].upper()

    def choose(
            self,
            choices,
            question=None,
            theme="choose",
            transform=None,
            humanize=True,
            default=None,
            **kwargs):
        """
        Args:
            choices (TYPE): Description
            question (None, optional): Description
            theme (str, optional): Description
            transform (None, optional): Description
            humanize (bool, optional): Description
            default (None, optional): Description

        Returns:
            TYPE: Description

        Raises:
            ValueError: Description
        """
        if not isinstance(choices, list):
            raise ValueError("Choices must be a list")
        if default:
            found = [
                num
                for num, def_choice in enumerate(choices)
                if def_choice == default
            ]
            if found:
                default_index = found[0] + 1
            else:
                raise ValueError("Default object not found in choices")
        else:
            default_index = None

        self.transform = transform
        i = 1
        self.text = ""
        for choice in choices:
            self.text += "{}. {}\n".format(
                i,
                self._humanize(choice, **kwargs) if humanize else choice
            )
            i += 1
        answered = False
        self.theme = theme
        self.prefix = False
        self._print()
        if self.format_only:
            return self.text
        if not question:
            question = _("Select")
        while not answered:
            try:
                ret = input(
                    "{} (1-{}){}: ".format(
                        question,
                        i - 1,
                        "[{}] ".format(default_index) if default_index else ""
                    )
                )
                if not ret and default_index:
                    ret = default_index
                    answered = True
                elif ret and int(ret) in range(1, i):
                    answered = True
            except ValueError:
                pass
        return choices[int(ret) - 1]

    def unitext(self, obj, theme=None, transform=None,
                humanize=True, **kwargs):
        """
        Function: unitext
        Summary: InsertHere
        Examples: InsertHere

        Attributes:
            Returns: InsertHere

        Args:
            obj (TYPE): Description
            theme (None, optional): Description
            transform (None, optional): Description
            humanize (bool, optional): Description

        Returns:
            TYPE: Description
        """
        self.transform = transform
        self.text = self._humanize(obj, **kwargs) if humanize else obj
        if hasattr(str, 'decode'):
            self.text = self.text.decode("utf-8")
        self.text = unidecode(self.text)
        self.theme = theme
        self.prefix = False
        return self._print()

    def slugify(self, obj, humanize=True, **kwargs):
        """Summary

        Args:
            obj (TYPE): Description
            humanize (bool, optional): Description

        Returns:
            TYPE: Description
        """
        self.text = self._humanize(obj, **kwargs) if humanize else obj
        if hasattr(str, 'decode'):
            self.text = self.text.decode("utf-8")
        self.text = unidecode(self.text)
        self.text = self.text.strip().replace(" ", "_")
        self.text = self.text.lower()
        return self.text

    def progress(self, count, total, prefix=_('Reading'), theme=None,
                 suffix=_('Complete'), barLength=50, **kwargs):
        """
        Args:
            count (TYPE): Description
            total (TYPE): Description
            prefix (str, optional): Description
            theme (None, optional): Description
            suffix (str, optional): Description
            barLength (int, optional): Description

        Returns:
            TYPE: Description
        """
        formatStr = "{0:.2f}"
        percents = formatStr.format(100 * (count / float(total)))
        filledLength = int(round(barLength * count / float(total)))
        bar = '█' * filledLength + '-' * (barLength - filledLength)
        self.text = '\r{} |{}| {}% {} '.format(prefix, bar, percents, suffix)

        if not self.format_only:
            sys.stdout.write(self.text)
            sys.stdout.flush()

        return self.text

    def load_theme(self, theme):
        """
        Function: load_theme
        Summary: InsertHere
        Examples: InsertHere

        Attributes:
            Returns: InsertHere

        Args:
            theme (TYPE): Description

        Raises:
            ValueError: Description
        """
        if not isinstance(theme, dict):
            raise ValueError("Theme must be a dict")
        for key in theme:
            self.theme_dict[key] = theme[key]

    def ask(
            self,
            obj,
            theme="warning",
            transform=None,
            humanize=True,
            validator=None,
            default=None,
            required=False,
            **kwargs):
        """Summary

        Args:
            obj (TYPE): Description
            theme (str, optional): Description
            transform (None, optional): Description
            humanize (bool, optional): Description
            validator (None, optional): Description
            default (None, optional): Description
            required (bool, optional): Description

        Returns:
            TYPE: Description

        Raises:
            ValueError: Description
        """
        self.transform = transform
        self.text = self._humanize(obj, **kwargs) if humanize else obj
        self.theme = theme
        self.prefix = None
        self.text = "{}{}".format(
            self.text,
            " [{}]: ".format(default) if default else " "
        )
        self._print(linebreak=False)

        if self.format_only:
            return self.text
        else:
            answered = False
            ask_text = ": "
            while not answered:
                data = input(ask_text)
                if required and not data and not default:
                    text = self.text
                    self.error(_("Value required"))
                    self.text = text
                else:
                    if validator and data:
                        if not callable(validator):
                            raise ValueError(
                                "Validator must be a function")
                        text = self.text
                        answered = validator(data)
                        if not answered:
                            ask_text = "Please answer again: "
                        self.text = text
                    else:
                        answered = True
            return data if data else default

    def clear(self):
        """Clear terminal."""
        os.system('cls' if os.name == 'nt' else 'clear')

    def select(self,
               obj,
               theme="choose",
               humanize=True,
               question=None,
               default=None,
               **kwargs):
        """Summary

        Args:
            obj (TYPE): Description
            theme (str, optional): Description
            humanize (bool, optional): Description
            question (None, optional): Description
            default (None, optional): Description

        Returns:
            TYPE: Description

        Raises:
            ValueError: Description
        """
        if not isinstance(obj, list):
            raise ValueError("select data must be a list")
        if default is not None:
            try:
                obj[default]
            except (ValueError, IndexError):
                raise ValueError("Select default not valid")
        self.transform = None
        options = [
            self._humanize(item, **kwargs) if humanize else item
            for item in obj
        ]
        phrases = []
        letters = []
        for opt in options:
            letter, phrase = self._get_letter(
                text=opt,
                optlist=letters
            )
            phrases.append(phrase)
            letters.append(letter)

        answered = False
        self.theme = theme
        self.prefix = False
        self.text = "{}: {}{}".format(
            question if question else _("Select"),
            ", ".join([text for text in phrases]),
            " [{}]".format(obj[default]) if default is not None else ""
        )
        self._print(linebreak=False)
        if self.format_only:
            return self.text
        while not answered:
            ret = input("? ")
            if not ret and default is not None:
                return default
            elif ret and ret.upper() in letters:
                answered = True
        return [
            index
            for index, data in enumerate(letters)
            if ret.upper() == data
        ][0]

    def _get_letter(self, text, index=0, optlist=[]):
        """Summary

        Args:
            text (TYPE): Description
            index (int, optional): Description
            optlist (list, optional): Description

        Returns:
            TYPE: Description

        Raises:
            ValueError: Description
        """
        try:
            test_letter = text[index].upper()
        except IndexError:
            raise ValueError("Can not found letter for {} option".format(text))
        if test_letter in optlist:
            index += 1
            test_letter, new_text = self._get_letter(text, index, optlist)
        else:
            new_text = "{}({}){}".format(
                text[:index] if index > 0 else "",
                test_letter,
                text[index + 1:]
            )
        return test_letter, new_text

    def run(
            self,
            task,
            title=None,
            get_stdout=False,
            run_stdout=False,
            verbose=False,
            silent=False,
            use_prefix=True,
            prefix="Cmd"):
        """Run command in subprocess.

        Args:
            task (string): command to run
            title (string, optional): title to be printed
            get_stdout (bool, optional): return stdout from command
            run_stdout (bool, optional): run stdout before command
            verbose (bool, optional): show command in terminal
            silent (bool, optional): occult stdout/stderr when running command

        Return
        ------
            Bool or String: Task success or Task stdout

        """
        if title:
            self.section(title)

        try:
            if run_stdout:
                if verbose:
                    self.info(task, use_prefix=use_prefix, prefix=prefix)
                command = subprocess.check_output(task, shell=True)

                if not command:
                    print('An error occur. Task aborted.')
                    return False

                if verbose:
                    self.info(command, use_prefix=use_prefix, prefix=prefix)
                ret = subprocess.call(command, shell=True)

            elif get_stdout is True:
                if verbose:
                    self.info(task, use_prefix=use_prefix, prefix=prefix)
                ret = subprocess.check_output(task, shell=True)
            else:
                if verbose:
                    self.info(task, use_prefix=use_prefix, prefix=prefix)
                ret = subprocess.call(
                    task if not silent else
                    "{} >/dev/null".format(task),
                    shell=True,
                    stderr=subprocess.STDOUT)

            if ret != 0 and not get_stdout:
                return False
        except BaseException:
            return False

        try:
            ret = ret.decode('utf-8')
        except AttributeError:
            pass

        return True if not get_stdout else ret