from typing import Union as _Union
from typing import List as _List
from typing import IO as _IO
from contextlib import contextmanager as _contextmanager
__all__ = ["Console", "Table"]
# Global rich.Console()
_console = None
# Global console theme
_theme = None
[docs]
class Table:
"""This a rich.table, with an additional "to_string()" function"""
def __init__(self, title=None, show_footer=False, show_edge=True):
from rich.table import Table as _Table
self._table = _Table(
title=title, show_edge=show_edge, show_footer=show_footer
)
[docs]
def add_column(
self, header, justify="center", style=None, no_wrap=False, footer=None
):
"""Add a column called 'header', with specified justification,
style and wrapping
"""
if footer is not None:
footer = str(footer)
self._table.add_column(
header=header,
style=style,
justify=justify,
no_wrap=no_wrap,
footer=footer,
)
[docs]
def add_row(self, row):
"""Add the passed row of data to the table"""
row = [str(x) if x is not None else None for x in row]
self._table.add_row(*row)
[docs]
def to_string(self):
"""Return this table rendered to a string"""
from rich.console import Console as _Console
console = _Console()
output = ""
for s in console.render(self._table):
output += s.text
return output
[docs]
class Console:
"""This is a singleton class that provides access to printing
and logging functions to the console. This uses 'rich'
for rich console printing
"""
[docs]
@staticmethod
def supports_emojis():
"""Return whether or not you can print emojis to this console"""
import sys
if sys.platform == "win32":
return False
else:
return True
[docs]
@staticmethod
def set_debugging_enabled(enabled, level=None):
"""Switch on or off debugging output"""
console = Console._get_console()
console._debugging_enabled = bool(enabled)
console._debugging_level = level
[docs]
@staticmethod
def set_theme(theme):
"""Set the theme used for the console - this should be
one of the themes in metawards.themes
"""
global _theme
if isinstance(theme, str):
if theme.lower().strip() == "simple":
from ._simple import Simple
_theme = Simple()
elif theme.lower().strip() == "default":
from ._spring_flowers import SpringFlowers
_theme = SpringFlowers()
else:
_theme = theme
@staticmethod
def _get_theme():
global _theme
if _theme is None:
from ._spring_flowers import SpringFlowers
_theme = SpringFlowers()
return _theme
@staticmethod
def _get_console():
global _console
if _console is None:
from rich.console import Console as _Console
theme = Console._get_theme()
_console = _Console(
record=True,
highlight=theme.should_highlight(),
highlighter=theme.highlighter(),
markup=theme.should_markup(),
log_time=True,
log_path=True,
emoji=Console.supports_emojis(),
)
_console._debugging_enabled = False
_console._debugging_level = None
# also install pretty traceback support
from rich.traceback import install as _install_rich
_install_rich()
return _console
[docs]
@staticmethod
@_contextmanager
def redirect_output(outdir: str, auto_bzip: bool = True):
"""Redirect all output and error to the directory 'outdir'"""
import os as os
import bz2
from rich.console import Console as _Console
outfile = os.path.join(outdir, "output.txt")
if auto_bzip:
outfile += ".bz2"
OUTFILE = bz2.open(outfile, "wt", encoding="utf-8")
else:
OUTFILE = open(outfile, "wt", encoding="utf-8")
console = Console._get_console()
if console is None:
raise AssertionError("The global console should never be None")
new_out = _Console(
file=OUTFILE,
record=False,
log_time=True,
log_path=True,
emoji=Console.supports_emojis(),
)
new_out._debugging_enabled = console._debugging_enabled
new_out._debugging_level = console._debugging_level
old_out = console
global _console
_console = new_out
try:
yield new_out
finally:
_console = old_out
OUTFILE.close()
[docs]
@staticmethod
def debugging_enabled(level: int = None):
"""Return whether debug output is enabled (optionally for
the specified level) - if not, then
anything sent to 'debug' (for that level) is not printed
"""
console = Console._get_console()
if not console._debugging_enabled:
return False
elif console._debugging_level is not None:
if level is None:
return True
else:
return level <= console._debugging_level
else:
return level is None
@staticmethod
def _retrieve_name(variable):
# thanks to scohe001 on stackoverflow
# https://stackoverflow.com/questions/18425225/getting-the-name-of-a-variable-as-a-string
import inspect
callers_local_vars = (
inspect.currentframe().f_back.f_back.f_locals.items()
)
return [
var_name
for var_name, var_val in callers_local_vars
if var_val is variable
][-1]
[docs]
@staticmethod
def debug(
text: str,
variables: _List[any] = None,
level: int = None,
markdown: bool = False,
**kwargs,
):
"""Print a debug string to the console. This will only be
printed if debugging is enabled. You can also print the
values of variables by passing them as a list to
'variables'
"""
if not Console.debugging_enabled(level=level):
return
if hasattr(text, "__call__"):
# the user passed in a lambda for delayed printing
text = text()
if not isinstance(text, str):
text = str(text)
if level is not None:
text = f"Level {level}: {text}"
console = Console._get_console()
if markdown:
from rich.markdown import Markdown as _Markdown
try:
text = _Markdown(text)
except Exception:
text = _Markdown(str(text))
console.log(text, justify="center", _stack_offset=2, **kwargs)
if variables is not None:
from rich.table import Table
from rich import box
import inspect
# get the local variables in the caller's scope
callers_local_vars = inspect.currentframe().f_back.f_locals.items()
table = Table(box=box.MINIMAL_DOUBLE_HEAD, style="on magenta")
table.add_column(
"Name", justify="right", style="cyan", no_wrap=True
)
table.add_column("Value", justify="left", style="green")
for variable in variables:
# get the name of the variable in the caller
try:
# go for the last matching variable in the caller's scope
name = [
var_name
for var_name, var_val in callers_local_vars
if var_val is variable
][-1]
except Exception:
name = "variable"
table.add_row(name, str(variable))
console.print(table)
[docs]
@staticmethod
def print(
text: str,
markdown: bool = False,
style: str = None,
markup: bool = None,
*args,
**kwargs,
):
"""Print to the console"""
if markdown:
from rich.markdown import Markdown as _Markdown
try:
text = _Markdown(text)
except Exception:
text = _Markdown(str(text))
theme = Console._get_theme()
style = theme.text(style)
try:
Console._get_console().print(text, style=style, markup=markup)
except UnicodeEncodeError:
# this output can't cope with a complex theme - switch
# to theme 'simple'
Console.set_theme("simple")
if isinstance(text, str):
# try to print this as latin-1
try:
text = text.encode("latin-1", errors="replace").decode(
"latin-1"
)
Console._get_console().print(text, markup=markup)
except Exception:
# accept that this isn't going to be printable
pass
[docs]
@staticmethod
def rule(title: str = None, style=None, **kwargs):
"""Write a rule across the screen with optional title"""
from rich.rule import Rule as _Rule
Console.print("")
theme = Console._get_theme()
style = theme.rule(style)
Console.print(_Rule(title, style=style))
[docs]
@staticmethod
def panel(
text: str,
markdown: bool = False,
width=None,
padding: bool = True,
style: str = None,
expand=True,
*args,
**kwargs,
):
"""Print within a panel to the console"""
from rich.panel import Panel as _Panel
if markdown:
from rich.markdown import Markdown as _Markdown
text = _Markdown(text)
theme = Console._get_theme()
padding_style = theme.padding_style(style)
style = theme.panel(style)
box = theme.panel_box(style)
from rich.padding import Padding as _Padding
if padding:
text = _Padding(text, (1, 2), style=padding_style)
else:
text = _Padding(text, (0, 1), style=padding_style)
Console.print(
_Panel(
text,
box=box,
width=width,
expand=expand,
style=style,
*args,
**kwargs,
)
)
[docs]
@staticmethod
def error(text: str, *args, **kwargs):
"""Print an error to the console"""
Console.rule("ERROR", style="error")
Console.print(text, style="error", *args, **kwargs)
Console.rule(style="error")
[docs]
@staticmethod
def warning(text: str, *args, **kwargs):
"""Print a warning to the console"""
Console.rule("WARNING", style="warning")
Console.print(text, style="warning", *args, **kwargs)
Console.rule(style="warning")
[docs]
@staticmethod
def info(text: str, *args, **kwargs):
"""Print an info section to the console"""
Console.rule("INFO", style="info")
Console.print(text, style="info", *args, **kwargs)
Console.rule(style="info")
@staticmethod
def center(text: str, *args, **kwargs):
from rich.text import Text as _Text
Console.print(_Text(str, justify="center"), *args, **kwargs)
@staticmethod
def command(text: str, *args, **kwargs):
Console.panel(text, style="command", padding=False)
@staticmethod
def print_profiler(profiler, *args, **kwargs):
Console.print(str(profiler))
[docs]
@staticmethod
def print_exception():
"""Print the current-handled exception"""
console = Console._get_console()
console.print_exception()
[docs]
@staticmethod
def save(file: _Union[str, _IO]):
"""Save the accumulated printing to the console to 'file'.
This can be a file or a filehandle. The buffer is
cleared after saving
"""
# get the console contents
try:
text = Console._get_console().export_text(clear=True, styles=False)
except Exception as e:
Console.error(f"Cannot get console output: {e.__class__} {e}")
return
if isinstance(file, str):
with open(file, "w", encoding="UTF-8") as FILE:
FILE.write(text)
else:
try:
if file.encoding.lower() != "utf-8":
# make sure that encoding the file to the right character
# set will replace invalid characters, rather than throw
# unicode encoding errors
text = text.encode(file.encoding, errors="replace").decode(
file.encoding
)
file.write(text)
except UnicodeEncodeError:
# this still didn't work - use a latin-1 round-robin
# to make everything ascii...
text = text.encode("latin-1", errors="replace").decode(
"latin-1"
)
file.write(text)