Skip to content

Commit

Permalink
Convert Printer from a singleton to improve test isolation.
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed Jan 15, 2024
1 parent 9ba17b1 commit 2330376
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 49 deletions.
7 changes: 4 additions & 3 deletions src/briefcase/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pathlib import Path

from briefcase.cmdline import parse_cmdline
from briefcase.console import Console, Log
from briefcase.console import Console, Log, Printer
from briefcase.exceptions import (
BriefcaseError,
BriefcaseTestSuiteFailure,
Expand All @@ -15,8 +15,9 @@
def main():
result = 0
command = None
logger = Log()
console = Console()
printer = Printer()
console = Console(printer=printer)
logger = Log(printer=printer)
try:
Command, extra_cmdline = parse_cmdline(sys.argv[1:])
command = Command(logger=logger, console=console)
Expand Down
94 changes: 53 additions & 41 deletions src/briefcase/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,32 +71,45 @@ class RichConsoleHighlighter(RegexHighlighter):
class Printer:
"""Interface for printing and managing output to the console and/or log."""

# Console to manage console output.
console = RichConsole(
highlighter=RichConsoleHighlighter(), emoji=False, soft_wrap=True
)
def __init__(self, width=180):
"""Create an interface for printing and managing output to the console and/or
log.
# Console to record all logging to a buffer while not printing anything to the console.
# We need to be wide enough to render `sdkmanager --list_installed` output without
# line wrapping.
LOG_FILE_WIDTH = 180
# Rich only records what's being logged if it is actually written somewhere;
# writing to /dev/null allows Rich to do so without needing to print the logs
# in the console or save them to file before it is known a file is wanted.
dev_null = open(os.devnull, "w", encoding="utf-8", errors="ignore")
log = RichConsole(
file=dev_null,
record=True,
width=LOG_FILE_WIDTH,
no_color=True,
markup=False,
emoji=False,
highlight=False,
soft_wrap=True,
)
The default width is wide enough to render the output of ``sdkmanager
--list_installed`` without line wrapping.
@classmethod
def __call__(cls, *messages, stack_offset=5, show=True, **kwargs):
:param width: The width at which content should be wrapped.
"""
self.width = width

# A wrapper around the console
self.console = RichConsole(
highlighter=RichConsoleHighlighter(),
emoji=False,
soft_wrap=True,
)

# Rich only records what's being logged if it is actually written somewhere;
# writing to /dev/null allows Rich to do so without needing to print the
# logs in the console or save them to file before it is known a file is
# wanted.
self.dev_null = open(os.devnull, "w", encoding="utf-8", errors="ignore")
self.log = RichConsole(
file=self.dev_null,
record=True,
width=self.width,
no_color=True,
color_system=None,
markup=False,
emoji=False,
highlight=False,
soft_wrap=True,
)

def __del__(self):
self.dev_null.close()

def __call__(self, *messages, stack_offset=5, show=True, **kwargs):
"""Entry point for all printing to the console and the log.
The log records all content that is printed whether it is shown in the console
Expand All @@ -111,23 +124,22 @@ def __call__(cls, *messages, stack_offset=5, show=True, **kwargs):
most uses are 5 levels deep from the actual logging.
"""
if show:
cls.to_console(*messages, **kwargs)
cls.to_log(*messages, stack_offset=stack_offset, **kwargs)
self.to_console(*messages, **kwargs)
self.to_log(*messages, stack_offset=stack_offset, **kwargs)

@classmethod
def to_console(cls, *messages, **kwargs):
def to_console(self, *messages, **kwargs):
"""Write only to the console and skip writing to the log."""
cls.console.print(*messages, **kwargs)
self.console.print(*messages, **kwargs)

@classmethod
def to_log(cls, *messages, stack_offset=5, **kwargs):
def to_log(self, *messages, stack_offset=5, **kwargs):
"""Write only to the log and skip writing to the console."""
cls.log.log(*map(sanitize_text, messages), _stack_offset=stack_offset, **kwargs)
self.log.log(
*map(sanitize_text, messages), _stack_offset=stack_offset, **kwargs
)

@classmethod
def export_log(cls):
def export_log(self):
"""Export the text of the entire log; the log is also cleared."""
return cls.log.export_text()
return self.log.export_text()


class RichLoggingStream:
Expand Down Expand Up @@ -162,8 +174,8 @@ class Log:
# subdirectory of command.base_path to store log files
LOG_DIR = "logs"

def __init__(self, printer=Printer(), verbosity: LogLevel = LogLevel.INFO):
self.print = printer
def __init__(self, printer=None, verbosity: LogLevel = LogLevel.INFO):
self.print = Printer() if printer is None else printer
# --verbosity flag: 0 for info, 1 for debug, 2 for deep debug
self.verbosity = verbosity
# --log flag to force logfile creation
Expand Down Expand Up @@ -390,7 +402,7 @@ def _build_log(self, command):
f"{thread} traceback:",
Traceback(
trace=stacktrace,
width=self.print.LOG_FILE_WIDTH,
width=self.print.width,
show_locals=True,
),
new_line_start=True,
Expand Down Expand Up @@ -447,11 +459,11 @@ def _build_log(self, command):


class Console:
def __init__(self, printer=Printer(), enabled=True):
def __init__(self, printer=None, enabled=True):
self.enabled = enabled
self.print = printer
self.print = Printer() if printer is None else printer
# Use Rich's input() to read from user
self.input = printer.console.input
self.input = self.print.console.input
self._wait_bar: Progress = None
# Signal that Rich is dynamically controlling the console output. Therefore,
# all output must be printed to the screen by Rich to prevent corruption of
Expand Down
15 changes: 10 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import inspect
import os
import subprocess
import time
from unittest.mock import ANY

import pytest

from briefcase.config import AppConfig
from briefcase.console import Printer

from .utils import create_file


def pytest_sessionfinish(session, exitstatus):
"""When pytest is wrapping up, close the /dev/null file handle for the logfile Rich
Console to avoid spurious ResourceWarning errors."""
Printer.dev_null.close()
def pytest_sessionstart(session):
"""Ensure that tests don't use a color console."""

os.environ["TERM"] = "dumb"
os.environ["NO_COLOR"] = "1"
try:
del os.environ["FORCE_COLOR"]
except KeyError:
pass


# alias so fixtures can still use them
Expand Down

0 comments on commit 2330376

Please sign in to comment.