Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2845.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Handle edge cases in the console more robustly.
47 changes: 28 additions & 19 deletions src/briefcase/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
import textwrap
import time
import traceback
from collections.abc import Callable, Collection, Iterable, Mapping
from collections.abc import Callable, Generator, Iterable, Mapping, Sequence
from contextlib import contextmanager
from datetime import datetime
from enum import IntEnum
from pathlib import Path
from typing import Any
from typing import Any, ClassVar

from rich.console import Console as RichConsole
from rich.control import strip_control_codes
Expand All @@ -31,7 +31,7 @@

from briefcase import __version__
from briefcase.config import parse_boolean
from briefcase.exceptions import InputDisabled
from briefcase.exceptions import BriefcaseError, InputDisabled

# Max width for printing to console; matches argparse's default width
MAX_TEXT_WIDTH = max(min(shutil.get_terminal_size().columns, 80) - 2, 20)
Expand Down Expand Up @@ -69,8 +69,8 @@ class RichConsoleHighlighter(RegexHighlighter):
consistent with a default HTML stylesheet, but otherwise renders content as-is.
"""

base_style = "repr."
highlights: Collection[str] = [
base_style: ClassVar[str] = "repr."
highlights: ClassVar[Sequence[str]] = [
r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)"
]

Expand Down Expand Up @@ -207,8 +207,9 @@ def __init__(
self.is_console_controlled = False

def close(self):
self._dev_null.close()
self._dev_null = None
if self._dev_null:
self._dev_null.close()
self._dev_null = None

def __del__(self):
if self._dev_null:
Expand Down Expand Up @@ -314,7 +315,7 @@ def warning_banner(
title: str | None = None,
message: str | None = None,
width: int = 80,
) -> str:
) -> None:
"""The title or message can be provided as a single or as multiline string. Any
common leading whitespace from each line will be removed.

Expand All @@ -326,7 +327,6 @@ def warning_banner(
:param title: The title of the box. If provided, appears centered at the top.
:param message: The message to format inside the box.
:param width: The total width of the box in characters. Defaults to 80.
:return: The formatted message enclosed in a bordered box.
"""
BORDER_LINE = "*" * width
lines_array = ["", BORDER_LINE]
Expand Down Expand Up @@ -514,13 +514,20 @@ def capture_stacktrace(self, label="Main thread"):
:param label: An identifying label for the thread that has raised the
stacktrace. Defaults to the main thread.
"""
exc_info = sys.exc_info()
try:
self.skip_log = exc_info[1].skip_logfile
except AttributeError:
pass
exc_type, exc_value, exc_trace = sys.exc_info()
if exc_value is None:
return

self.stacktraces.append((label, Traceback.extract(*exc_info, show_locals=True)))
if isinstance(exc_value, BriefcaseError):
self.skip_log = exc_value.skip_logfile

trace = Traceback.extract(
exc_type, # ty:ignore[invalid-argument-type]
exc_value,
exc_trace,
show_locals=True,
)
self.stacktraces.append((label, trace))

def add_log_file_extra(self, func: Callable[[], object]):
"""Register a function to be called in the event that a log file is written.
Expand Down Expand Up @@ -662,7 +669,7 @@ def is_interactive(self):
should be specifically disabled in non-interactive sessions.
"""
# `sys.__stdout__` is used because Rich captures and redirects `sys.stdout`
return os.isatty(sys.__stdout__.fileno())
return sys.__stdout__ is not None and os.isatty(sys.__stdout__.fileno())

@property
def is_color_enabled(self):
Expand Down Expand Up @@ -699,7 +706,7 @@ def wait_bar(
*,
transient: bool = False,
markup: bool = False,
) -> NotDeadYet:
) -> Generator[NotDeadYet]:
"""Activates the Wait Bar as a context manager.

If the Wait Bar is already active, then its message is updated for the new
Expand Down Expand Up @@ -774,7 +781,9 @@ def release_console_control(self):
"""
# Preserve current console state
is_output_controlled = self.is_console_controlled
is_wait_bar_running = self._wait_bar and self._wait_bar.live.is_started
is_wait_bar_running = (
self._wait_bar is not None and self._wait_bar.live.is_started
)

# Stop any active dynamic console elements
if is_wait_bar_running:
Expand Down Expand Up @@ -842,7 +851,7 @@ def input(self, prompt: str, *, markup: bool = False):

return input_value

def input_boolean(self, question: str, default: bool = False) -> bool:
def input_boolean(self, question: str, default: bool | None = False) -> bool:
"""Get a boolean input from user, in the form of y/n.

The user might press "y" for true or "n" for false. If input is disabled,
Expand Down
13 changes: 13 additions & 0 deletions tests/console/test_Log.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,19 @@ def test_capture_stacktrace_for_briefcaseerror(console, skip_logfile):
assert console.skip_log is skip_logfile


def test_capture_stacktrace_no_exception(console):
"""capture_stacktrace is a no-op when there is no active exception."""
console.capture_stacktrace()

assert console.stacktraces == []


def test_close_is_idempotent(console):
"""Close() can be called more than once."""
console.close()
console.close()


def test_save_log_to_file_do_not_log(console, command):
"""Nothing is done to save log if no command or --log wasn't passed."""
console.save_log_to_file(command=None)
Expand Down