class Presentation: def __init__(self, content): self.slides = [ slide_content.strip() for slide_content in content.split("---\n") ] print(self.slides) for i in range(1, len(self.slides)): self.slides[i] = "# " + self.slides[i] self.index = 0 self.live = Live() def start(self): self.live.start() self.update() def stop(self): self.live.stop() def next(self): if self.index >= self.__len__() - 1: self.stop() return self.index = self.index + 1 self.update() def update(self): self.live.update( Layout(Padding(Markdown(self.slides[self.index]), (2, 2)))) @property def started(self): return self.live.is_started def __len__(self): return len(self.slides)
def live_table(self, refresh_rate: int = 5) -> ContextManager[Live]: """Context manager providing a live UI which can be updated dynamically""" live_table = Live(refresh_per_second=refresh_rate) live_table.start() try: yield live_table finally: live_table.stop()
class CommonBarManager: def __init__(self): self._table = Table.grid() self._live = Live(self._table, console=console) async def close(): self._live.stop() on_close_callbacks.append(close) self._live.start() def get_bar(self): bar = create_bar() tree = Tree(bar) self._table.add_row(tree) return CommonBar(tree, bar)
class OverallBarManager: def __init__(self): self._bar = create_bar() self._tree = Tree(self._bar) self._live = Live(self._tree, console=console) async def close(): self._live.stop() on_close_callbacks.append(close) self._live.start() self._overall_bar = OverallBar(self._bar) def get_bar(self): return VirtualBar(self._tree, self._overall_bar)
class RichTablePrinter(object): def __init__(self, fields={}, key=None): """ Logger based on `rich` tables :param key: str or None main key to group results by row :param fields: dict of (dict or False) Field descriptors containing goal ("lower_is_better" or "higher_is_better"), format and display name The key is a regex that will be used to match the fields to log """ self.fields = dict(fields) self.key = key self.key_to_row_idx = {} self.name_to_column_idx = {} self.table = None self.console = None self.live = None self.best = {} if key is not None and key not in self.fields: self.fields = {key: {}, **fields} def _repr_html_(self) -> str: if self.console is None: return "Empty table" segments = list(self.console.render(self.table, self.console.options)) # type: ignore html = _render_segments(segments) return html def log(self, info): if self.table is None: is_in_notebook = check_is_in_notebook() self.table = Table() self.console = Console(width=2 ** 32 - 1 if is_in_notebook else None) if is_in_notebook: dh = display(None, display_id=True) self.refresh = lambda: dh.update(self) else: self.live = Live(self.table, console=self.console, auto_refresh=False) self.live.start() self.refresh = lambda: self.live.refresh() # self.console = Console() # table_centered = Columns((self.table,), align="center", expand=True) # self.live = Live(table_centered, console=console) # self.live.start() for name, value in info.items(): if name not in self.name_to_column_idx: matcher, column_name = get_last_matching_value(self.fields, name, "name", default=name) if column_name is False: self.name_to_column_idx[name] = -1 continue self.table.add_column(re.sub(matcher, column_name, name) if matcher is not None else name, no_wrap=True) self.table.columns[-1]._cells = [''] * (len(self.table.columns[0]._cells) if len(self.table.columns) else 0) self.name_to_column_idx[name] = (max(self.name_to_column_idx.values()) + 1) if len(self.name_to_column_idx) else 0 new_name_to_column_idx = {} columns = [] def get_name_index(name): try: return get_last_matching_index(self.fields, name) except ValueError: return len(self.name_to_column_idx) for name in sorted(self.name_to_column_idx.keys(), key=get_name_index): if self.name_to_column_idx[name] >= 0: columns.append(self.table.columns[self.name_to_column_idx[name]]) new_name_to_column_idx[name] = (max(new_name_to_column_idx.values()) + 1) if len(new_name_to_column_idx) else 0 else: new_name_to_column_idx[name] = -1 self.table.columns = columns self.name_to_column_idx = new_name_to_column_idx if self.key is not None and self.key in info and info[self.key] in self.key_to_row_idx: idx = self.key_to_row_idx[info[self.key]] elif self.key is not None and self.key not in info and self.key_to_row_idx: idx = list(self.key_to_row_idx.values())[-1] else: self.table.add_row() idx = len(self.table.rows) - 1 if self.key is not None: self.key_to_row_idx[info[self.key]] = idx for name, value in info.items(): if self.name_to_column_idx[name] < 0: continue formatted_value = get_last_matching_value(self.fields, name, "format", "{}")[1].format(value) goal = get_last_matching_value(self.fields, name, "goal", None)[1] if goal is not None: if name not in self.best: self.best[name] = value else: diff = (value - self.best[name]) * (-1 if goal == "lower_is_better" else 1) if diff > 0: self.best[name] = value formatted_value = "[green]" + formatted_value + "[/green]" elif diff <= 0: formatted_value = "[red]" + formatted_value + "[/red]" self.table.columns[self.name_to_column_idx[name]]._cells[idx] = formatted_value self.refresh() def finalize(self): if self.live is not None: self.live.stop()
class RichUI(UI): """ A UI that uses the rich terminal library """ def __init__(self) -> None: self._rich_live: Live def __enter__(self) -> 'RichUI': self._rich_live = Live(Spinner("bouncingBar", ""), refresh_per_second=16) self._rich_live.start() return self def __exit__(self, *args: Any, **kwargs: Any) -> None: self._rich_live.stop() def update(self, job: SlurmJobStatus) -> None: self._rich_live.update(self._make_table(job)) def error(self, text: str) -> None: self._rich_live.console.print(":cross_mark:", text, style="bold red", emoji=True) def info(self, text: str) -> None: self._rich_live.console.print(":information_source:", text, style="bold blue", emoji=True) def success(self, text: str) -> None: self._rich_live.console.print(":heavy_check_mark: ", text, style="bold green", emoji=True) def launch(self, text: str) -> None: self._rich_live.console.print(":rocket: ", text, style="bold yellow", emoji=True) def _make_table(self, job: SlurmJobStatus) -> Table: table = Table(style="bold", box=box.MINIMAL) table.add_column("ID") table.add_column("Name") table.add_column("State") for task in job.tasks: last_column: RenderableType = task.state color = "grey42" if task.state == "RUNNING": color = "blue" last_column = Spinner("arc", task.state) elif task.state == "COMPLETED": color = "green" last_column = f":heavy_check_mark: {task.state}" elif task.state == "FAILED": color = "red" last_column = f":cross_mark: {task.state}" table.add_row(str(task.id), task.name, last_column, style=color) return table
class Log: STATUS_WIDTH = 11 def __init__(self) -> None: self.console = Console(highlight=False) self._crawl_progress = Progress( TextColumn("{task.description}", table_column=Column(ratio=1)), BarColumn(), TimeRemainingColumn(), expand=True, ) self._download_progress = Progress( TextColumn("{task.description}", table_column=Column(ratio=1)), TransferSpeedColumn(), DownloadColumn(), BarColumn(), TimeRemainingColumn(), expand=True, ) self._live = Live(console=self.console, transient=True) self._update_live() self._showing_progress = False self._progress_suspended = False self._lock = asyncio.Lock() self._lines: List[str] = [] # Whether different parts of the output are enabled or disabled self.output_explain = False self.output_status = True self.output_report = True def _update_live(self) -> None: elements = [] if self._crawl_progress.task_ids: elements.append(self._crawl_progress) if self._download_progress.task_ids: elements.append(self._download_progress) group = Group(*elements) self._live.update(group) @contextmanager def show_progress(self) -> Iterator[None]: if self._showing_progress: raise RuntimeError( "Calling 'show_progress' while already showing progress") self._showing_progress = True try: with self._live: yield finally: self._showing_progress = False @asynccontextmanager async def exclusive_output(self) -> AsyncIterator[None]: if not self._showing_progress: raise RuntimeError( "Calling 'exclusive_output' while not showing progress") async with self._lock: self._progress_suspended = True self._live.stop() try: yield finally: self._live.start() self._progress_suspended = False for line in self._lines: self.print(line) self._lines = [] def unlock(self) -> None: """ Get rid of an exclusive output state. This function is meant to let PFERD print log messages after the event loop was forcibly stopped and if it will not be started up again. After this is called, it is not safe to use any functions except the logging functions (print, warn, ...). """ self._progress_suspended = False for line in self._lines: self.print(line) def print(self, text: str) -> None: """ Print a normal message. Allows markup. """ if self._progress_suspended: self._lines.append(text) else: self.console.print(text) # TODO Print errors (and warnings?) to stderr def warn(self, text: str) -> None: """ Print a warning message. Allows no markup. """ self.print(f"[bold bright_red]Warning[/] {escape(text)}") def warn_contd(self, text: str) -> None: """ Print further lines of a warning message. Allows no markup. """ self.print(f"{escape(text)}") def error(self, text: str) -> None: """ Print an error message. Allows no markup. """ self.print(f"[bold bright_red]Error[/] [red]{escape(text)}") def error_contd(self, text: str) -> None: """ Print further lines of an error message. Allows no markup. """ self.print(f"[red]{escape(text)}") def unexpected_exception(self) -> None: """ Call this in an "except" clause to log an unexpected exception. """ t, v, tb = sys.exc_info() if t is None or v is None or tb is None: # We're not currently handling an exception, so somebody probably # called this function where they shouldn't. self.error("Something unexpected happened") self.error_contd("") for line in traceback.format_stack(): self.error_contd(line[:-1]) # Without the newline self.error_contd("") else: self.error("An unexpected exception occurred") self.error_contd("") self.error_contd(traceback.format_exc()) # Our print function doesn't take types other than strings, but the # underlying rich.print function does. This call is a special case # anyways, and we're calling it internally, so this should be fine. self.print( Panel.fit(""" Please copy your program output and send it to the PFERD maintainers, either directly or as a GitHub issue: https://github.com/Garmelon/PFERD/issues/new """.strip())) # type: ignore def explain_topic(self, text: str) -> None: """ Print a top-level explain text. Allows no markup. """ if self.output_explain: self.print(f"[yellow]{escape(text)}") def explain(self, text: str) -> None: """ Print an indented explain text. Allows no markup. """ if self.output_explain: self.print(f" {escape(text)}") def status(self, style: str, action: str, text: str, suffix: str = "") -> None: """ Print a status update while crawling. Allows markup in the "style" argument which will be applied to the "action" string. """ if self.output_status: action = escape(f"{action:<{self.STATUS_WIDTH}}") self.print(f"{style}{action}[/] {escape(text)} {suffix}") def report(self, text: str) -> None: """ Print a report after crawling. Allows markup. """ if self.output_report: self.print(text) @contextmanager def _bar( self, progress: Progress, description: str, total: Optional[float], ) -> Iterator[ProgressBar]: if total is None: # Indeterminate progress bar taskid = progress.add_task(description, start=False) else: taskid = progress.add_task(description, total=total) self._update_live() try: yield ProgressBar(progress, taskid) finally: progress.remove_task(taskid) self._update_live() def crawl_bar( self, style: str, action: str, text: str, total: Optional[float] = None, ) -> ContextManager[ProgressBar]: """ Allows markup in the "style" argument which will be applied to the "action" string. """ action = escape(f"{action:<{self.STATUS_WIDTH}}") description = f"{style}{action}[/] {text}" return self._bar(self._crawl_progress, description, total) def download_bar( self, style: str, action: str, text: str, total: Optional[float] = None, ) -> ContextManager[ProgressBar]: """ Allows markup in the "style" argument which will be applied to the "action" string. """ action = escape(f"{action:<{self.STATUS_WIDTH}}") description = f"{style}{action}[/] {text}" return self._bar(self._download_progress, description, total)
class ProgressBarRich(ProgressBarBase): def __init__(self, min_value, max_value, title=None, progress=None, indent=0, parent=None): super(ProgressBarRich, self).__init__(min_value, max_value, title=title) import rich.progress import rich.table import rich.tree self.console = rich.console.Console(record=True) self.parent = parent if progress is None: self.progress = rich.progress.Progress( rich.progress.SpinnerColumn(), rich.progress.TextColumn("[progress.description]{task.description}"), rich.progress.BarColumn(), rich.progress.TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), # rich.progress.TimeRemainingColumn(), TimeElapsedColumn(), rich.progress.TextColumn("[red]{task.fields[status]}"), console=self.console, transient=False, expand=False, ) else: self.progress = progress if parent is None: self.node = rich.tree.Tree(self.progress) from rich.live import Live self.live = Live(self.node, refresh_per_second=5, console=self.console) else: self.node = parent.add(self.progress) # we do 1000 discrete steps self.steps = 0 self.indent = indent padding = max(0, 45- (self.indent * 4) - len(self.title)) self.passes = None self.task = self.progress.add_task(f"[red]{self.title}" + (" " * padding), total=1000, start=False, status=self.status or '', passes=self.passes) self.started = False self.subtasks = [] def add_child(self, parent, task, title): return ProgressBarRich(self.min_value, self.max_value, title, indent=self.indent+1, parent=self.node) def __call__(self, value): if not self.started: self.progress.start_task(self.task) if value > self.value: steps = int(value * 1000) delta = steps - self.steps if delta > 0: self.progress.update(self.task, advance=delta, refresh=False, passes=self.passes) else: start_time = self.progress.tasks[0].start_time self.progress.reset(self.task, completed=steps, refresh=False, status=self.status or '') self.progress.tasks[0].start_time = start_time self.steps = steps self.value = value def update(self, value): self(value) def finish(self): self(self.max_value) if self.parent is None: self.live.refresh() def start(self): if self.parent is None and not self.live.is_started: self.live.refresh() self.live.start() def exit(self): if self.parent is None: self.live.stop() def set_status(self, status): self.status = status self.progress.update(self.task, status=self.status) def set_passes(self, passes): self.passes = passes