class Printer: """ Pytoil's default CLI output printer, designed for user friendly, colourful output, not for logging. """ _pytoil_theme = Theme( styles={ "title": Style(color="bright_cyan", bold=True), "info": Style(color="bright_cyan"), "warning": Style(color="yellow", bold=True), "error": Style(color="bright_red", bold=True), "error_message": Style(color="white", bold=True), "good": Style(color="bright_green"), "note": Style(color="white", bold=True), "subtle": Style(color="bright_black", italic=True), }) _pytoil_console = Console(theme=_pytoil_theme) __slots__ = () def title(self, msg: str, spaced: bool = True) -> None: """ Print a bold title message or section header. """ to_print = f"{msg}" if spaced: to_print = f"{msg}\n" self._pytoil_console.print(to_print, style="title") def warn(self, msg: str, exits: int | None = None) -> None: """ Print a warning message. If `exits` is not None, will call `sys.exit` with given code. """ self._pytoil_console.print(f"⚠️ {msg}", style="warning") if exits is not None: sys.exit(exits) def info(self, msg: str, exits: int | None = None, spaced: bool = False) -> None: """ Print an info message. If `exits` is not None, will call `sys.exit` with given code. If spaced is True, a new line will be printed before and after the message. """ to_print = f"💡 {msg}" if spaced: to_print = f"\n💡 {msg}\n" self._pytoil_console.print(to_print, style="info") if exits is not None: sys.exit(exits) def sub_info(self, msg: str, exits: int | None = None) -> None: """ Print a sub-info message. If `exits` is not None, will call `sys.exit` with given code. """ self._pytoil_console.print(f" ↪ {msg}") if exits is not None: sys.exit(exits) def error(self, msg: str, exits: int | None = None) -> None: """ Print an error message. If `exits` is not None, will call `sys.exit` with given code. """ self._pytoil_console.print( f"[error]✘ Error: [/error][error_message]{msg}[/error_message]") if exits is not None: sys.exit(exits) def good(self, msg: str, exits: int | None = None) -> None: """ Print a success message. If `exits` is not None, will call `sys.exit` with given code. """ self._pytoil_console.print(f"✔ {msg}", style="good") if exits is not None: sys.exit(exits) def note(self, msg: str, exits: int | None = None) -> None: """ Print a note, designed for supplementary info on another printer method. If `exits` is not None, will call `sys.exit` with given code. """ self._pytoil_console.print(f"[note]Note:[/note] {msg}") if exits is not None: sys.exit(exits) def text(self, msg: str, exits: int | None = None) -> None: """ Print default text. If `exits` is not None, will call `sys.exit` with given code. """ self._pytoil_console.print(msg, style="default") if exits is not None: sys.exit(exits) def progress(self) -> Progress: """ Return a pre-configured rich spinner. """ text_column = TextColumn("{task.description}") spinner_column = SpinnerColumn("simpleDotsScrolling", style="bold white") return Progress(text_column, spinner_column, transient=True) def subtle(self, msg: str) -> None: """ Print subtle greyed out text. """ self._pytoil_console.print(msg, style="subtle", markup=None)
def test_color_property(): assert Style(color="red").color == Color("red", ColorType.STANDARD, 1, None)
def test_parse(): assert Style.parse("") == Style() assert Style.parse("red") == Style(color="red") assert Style.parse("not bold") == Style(bold=False) assert Style.parse("bold red on black") == Style(color="red", bgcolor="black", bold=True) assert Style.parse("bold link https://example.org") == Style( bold=True, link="https://example.org") with pytest.raises(errors.StyleSyntaxError): Style.parse("on") with pytest.raises(errors.StyleSyntaxError): Style.parse("on nothing") with pytest.raises(errors.StyleSyntaxError): Style.parse("rgb(999,999,999)") with pytest.raises(errors.StyleSyntaxError): Style.parse("not monkey") with pytest.raises(errors.StyleSyntaxError): Style.parse("link")
def test_eq(): assert Style(bold=True, color="red") == Style(bold=True, color="red") assert Style(bold=True, color="red") != Style(bold=True, color="green") assert Style().__eq__("foo") == NotImplemented
def test_empty(): assert Style.null() == Style()
def test_add(): assert Style(color="red") + None == Style(color="red") assert Style().__add__("foo") == NotImplemented
def test_pick_first(): with pytest.raises(ValueError): Style.pick_first()
def test_render_size(): console = Console(width=63, height=46, legacy_windows=False) options = console.options.update_dimensions(80, 4) lines = console.render_lines(Panel("foo", title="Hello"), options=options) print(repr(lines)) expected = [ [ Segment("╭─", Style()), Segment( "────────────────────────────────── Hello ───────────────────────────────────" ), Segment("─╮", Style()), ], [ Segment("│", Style()), Segment(" ", Style()), Segment("foo"), Segment( " " ), Segment(" ", Style()), Segment("│", Style()), ], [ Segment("│", Style()), Segment(" ", Style()), Segment( " ", Style(), ), Segment(" ", Style()), Segment("│", Style()), ], [ Segment( "╰──────────────────────────────────────────────────────────────────────────────╯", Style(), ) ], ] assert lines == expected
def test_strip_styles(): segments = [Segment("foo", Style(bold=True))] assert list(Segment.strip_styles(segments)) == [Segment("foo", None)]
def test_get_style_at_offset(): console = Console() text = Text.from_markup("Hello [b]World[/b]") assert text.get_style_at_offset(console, 0) == Style() assert text.get_style_at_offset(console, 6) == Style(bold=True)
def test_background_style(): assert Style(bold=True, color="yellow", bgcolor="red").background_style == Style(bgcolor="red")
def get_style(text: str) -> Style: return Style.parse( f"bold yellow link https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword={text}" )
def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespace) -> None: root_logger = logging.getLogger() base_logger = logging.getLogger("red") base_logger.setLevel(level) dpy_logger = logging.getLogger("discord") dpy_logger.setLevel(logging.WARNING) warnings_logger = logging.getLogger("py.warnings") warnings_logger.setLevel(logging.WARNING) rich_console = rich.get_console() rich.reconfigure(tab_size=4) rich_console.push_theme( Theme( { "log.time": Style(dim=True), "logging.level.warning": Style(color="yellow"), "logging.level.critical": Style(color="white", bgcolor="red"), "repr.number": Style(color="cyan"), "repr.url": Style(underline=True, italic=True, bold=False, color="cyan"), } ) ) rich_console.file = sys.stdout # This is terrible solution, but it's the best we can do if we want the paths in tracebacks # to be visible. Rich uses `pygments.string` style which is fine, but it also uses # this highlighter which dims most of the path and therefore makes it unreadable on Mac. PathHighlighter.highlights = [] enable_rich_logging = False if isatty(0) and cli_flags.rich_logging is None: # Check if the bot thinks it has a active terminal. enable_rich_logging = True elif cli_flags.rich_logging is True: enable_rich_logging = True file_formatter = logging.Formatter( "[{asctime}] [{levelname}] {name}: {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{" ) if enable_rich_logging is True: rich_formatter = logging.Formatter("{message}", datefmt="[%X]", style="{") stdout_handler = RedRichHandler( rich_tracebacks=True, show_path=False, highlighter=NullHighlighter(), tracebacks_extra_lines=cli_flags.rich_traceback_extra_lines, tracebacks_show_locals=cli_flags.rich_traceback_show_locals, tracebacks_theme=( PygmentsSyntaxTheme(FixedMonokaiStyle) if rich_console.color_system == "truecolor" else ANSISyntaxTheme(SYNTAX_THEME) ), ) stdout_handler.setFormatter(rich_formatter) else: stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(file_formatter) root_logger.addHandler(stdout_handler) logging.captureWarnings(True) if not location.exists(): location.mkdir(parents=True, exist_ok=True) # Rotate latest logs to previous logs previous_logs: List[pathlib.Path] = [] latest_logs: List[Tuple[pathlib.Path, str]] = [] for path in location.iterdir(): match = re.match(r"latest(?P<part>-part\d+)?\.log", path.name) if match: part = match.groupdict(default="")["part"] latest_logs.append((path, part)) match = re.match(r"previous(?:-part\d+)?.log", path.name) if match: previous_logs.append(path) # Delete all previous.log files for path in previous_logs: path.unlink() # Rename latest.log files to previous.log for path, part in latest_logs: path.replace(location / f"previous{part}.log") latest_fhandler = RotatingFileHandler( stem="latest", directory=location, maxBytes=1_000_000, # About 1MB per logfile backupCount=MAX_OLD_LOGS, encoding="utf-8", ) all_fhandler = RotatingFileHandler( stem="red", directory=location, maxBytes=1_000_000, backupCount=MAX_OLD_LOGS, encoding="utf-8", ) for fhandler in (latest_fhandler, all_fhandler): fhandler.setFormatter(file_formatter) root_logger.addHandler(fhandler)
for i in range(1, self.backupCount + 1): next_log = self.directory / f"{self.baseStem}-part{i + 1}.log" if next_log.exists(): prev_log = self.directory / f"{self.baseStem}-part{i}.log" next_log.replace(prev_log) else: # Simply start a new file self.baseFilename = str( self.directory / f"{self.baseStem}-part{latest_part_num + 1}.log" ) self.stream = self._open() SYNTAX_THEME = { Token: Style(), Comment: Style(color="bright_black"), Keyword: Style(color="cyan", bold=True), Keyword.Constant: Style(color="bright_magenta"), Keyword.Namespace: Style(color="bright_red"), Operator: Style(bold=True), Operator.Word: Style(color="cyan", bold=True), Name.Builtin: Style(bold=True), Name.Builtin.Pseudo: Style(color="bright_red"), Name.Exception: Style(bold=True), Name.Class: Style(color="bright_green"), Name.Function: Style(color="bright_green"), String: Style(color="yellow"), Number: Style(color="cyan"), Error: Style(bgcolor="red"), }
def test_render(): assert Style(color="red").render("foo", color_system=None) == "foo" assert (Style(color="red", bgcolor="black", bold=True).render("foo") == "\x1b[1;31;40mfoo\x1b[0m") assert Style().render("foo") == "foo"
def test_strip_links(): segments = [ Segment("foo", Style(bold=True, link="https://www.example.org")) ] assert list( Segment.strip_links(segments)) == [Segment("foo", Style(bold=True))]
def test_test(): Style(color="red").test("hello")
def test_is_control(): assert Segment("foo", Style(bold=True)).is_control == False assert Segment("foo", Style(bold=True), []).is_control == True assert Segment("foo", Style(bold=True), [(ControlType.HOME, 0)]).is_control == True
def test_iadd(): style = Style(color="red") style += Style(bold=True) assert style == Style(color="red", bold=True) style += None assert style == Style(color="red", bold=True)
def output_table(templates_to_print: list, handle: str) -> None: """ Output a nice looking, rich rendered table. :param templates_to_print: The templates tht should go into the table :param handle: The handle the user inputted """ for template in templates_to_print: template[2] = TemplateInfo.set_linebreaks(template[2]) log.debug('Building info table.') table = Table(title=f'[bold]Info on cookietemple´s {handle}', title_style="blue", header_style=Style(color="blue", bold=True), box=HEAVY_HEAD) table.add_column("Name", justify="left", style="green", no_wrap=True) table.add_column("Handle", justify="left") table.add_column("Long Description", justify="left") table.add_column("Available Libraries", justify="left") table.add_column("Version", justify="left") for template in templates_to_print: table.add_row(f'[bold]{template[0]}', template[1], f'{template[2]}\n', template[3], template[4]) log.debug('Printing info table.') console = Console() console.print(table)
def test_repr(): assert repr(Style(bold=True, color="red")) == 'Style.parse("bold red")'
def test_get_style(): console = Console() console.get_style("repr.brace") == Style(bold=True)
def test_hash(): assert isinstance(hash(Style()), int)
def test_get_style_default(): console = Console() console.get_style("foobar", default="red") == Style(color="red")
def test_bool(): assert bool(Style()) is False assert bool(Style(bold=True)) is True assert bool(Style(color="red")) is True assert bool(Style.parse("")) is False
def test_chain(): assert Style.chain(Style(color="red"), Style(bold=True)) == Style(color="red", bold=True)
def test_bgcolor_property(): assert Style(bgcolor="black").bgcolor == Color("black", ColorType.STANDARD, 0, None)
def test_copy(): style = Style(color="red", bgcolor="black", italic=True) assert style == style.copy() assert style is not style.copy()
def test_pygments_syntax_theme(): style = PygmentsSyntaxTheme("default") assert style.get_style_for_token("abc") == Style.parse("none")
def test_repr(): assert repr(Style(bold=True, color="red")) == '<style "bold red">'