feat(framework): Use rich to render colors, fixing color-rendering issues on Windows#6626
feat(framework): Use rich to render colors, fixing color-rendering issues on Windows#6626
rich to render colors, fixing color-rendering issues on Windows#6626Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates Flower’s Python console logging to use rich for color rendering, aiming to fix color output issues on Windows terminals.
Changes:
- Replace ANSI escape-code coloring with
richstyles for console log level rendering. - Rework
ConsoleHandlerto render and print formatted log output viarich.Console. - Simplify
update_console_handlerto update the module’s globalconsole_handlerdirectly.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| message = MESSAGE_FORMATTER.format(record) | ||
| formatted_time = TIME_FORMATTER.formatTime(record) | ||
| if self.json: | ||
| record.message = record.getMessage().replace("\t", "").strip() | ||
|
|
||
| # Check if the message is empty | ||
| if not record.message: | ||
| message = message.replace("\t", "").strip() | ||
| if not message: | ||
| return | ||
|
|
||
| super().emit(record) | ||
|
|
||
| def format(self, record: LogRecord) -> str: | ||
| """Format function that adds colors to log level.""" | ||
| seperator = " " * (8 - len(record.levelname)) | ||
| if self.json: | ||
| log_fmt = "{lvl='%(levelname)s', time='%(asctime)s', msg='%(message)s'}" | ||
| renderable: str | Text = f"{{lvl='{record.levelname}', " | ||
| renderable += f"time='{formatted_time}', msg='{message}'}}" | ||
| else: | ||
| log_fmt = ( | ||
| f"{LOG_COLORS[record.levelname] if self.colored else ''}" | ||
| f"%(levelname)s {'%(asctime)s' if self.timestamps else ''}" | ||
| f"{LOG_COLORS['RESET'] if self.colored else ''}" | ||
| f": {seperator} %(message)s" | ||
| ) | ||
| formatter = logging.Formatter(log_fmt) | ||
| return formatter.format(record) | ||
| separator = " " * (8 - len(record.levelname)) | ||
| timestamp = f" {formatted_time}" if self.timestamps else "" | ||
| head = f"{record.levelname}{timestamp}" |
There was a problem hiding this comment.
formatted_time = TIME_FORMATTER.formatTime(record) is computed for every record even when self.json is false and self.timestamps is false, where the value is unused. Moving time formatting inside the branches that need it would avoid unnecessary strftime/time conversion work in the hot path.
| markup=False, | ||
| no_color=not colored, | ||
| ) | ||
|
|
There was a problem hiding this comment.
Console is initialized with file=self.stream, but elsewhere in this module console_handler.stream is reassigned (e.g., mirror_output_to_queue, redirect_output, restore_output) to capture/redirect output. rich.Console keeps its own file reference, so log output will continue going to the original stream instead of the updated handler.stream, which can break CLI JSON output capture/redirection. Consider overriding setStream/adding a setter that updates self.console.file whenever self.stream changes, or re-create/update the Console in emit() based on the current stream.
| def setStream(self, stream: TextIO | None) -> None: # type: ignore[override] | |
| """Set the stream and keep the Console's file in sync.""" | |
| super().setStream(stream) | |
| # Ensure the rich Console writes to the same stream as the handler. | |
| # logging.StreamHandler.setStream sets self.stream, which may be None. | |
| # rich.Console expects a file-like object; if self.stream is None, | |
| # Console will continue using its existing file. | |
| if hasattr(self, "console") and self.stream is not None: | |
| self.console.file = self.stream |
No description provided.