From c640c35254589c811e26ecfa90ba7f8e6a3688c3 Mon Sep 17 00:00:00 2001 From: Tridwoxi Date: Mon, 18 May 2026 22:12:05 -0400 Subject: [PATCH 1/3] Fix ty typing errors in console.py --- src/briefcase/console.py | 47 +++++++++++++++++++++++---------------- tests/console/test_Log.py | 13 +++++++++++ 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/briefcase/console.py b/src/briefcase/console.py index 603f6f027..fc05cc28a 100644 --- a/src/briefcase/console.py +++ b/src/briefcase/console.py @@ -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 @@ -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) @@ -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(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)" ] @@ -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: @@ -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. @@ -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] @@ -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 + _, exception, _ = sys.exc_info() + if exception is None: + return - self.stacktraces.append((label, Traceback.extract(*exc_info, show_locals=True))) + if isinstance(exception, BriefcaseError): + self.skip_log = exception.skip_logfile + + trace = Traceback.extract( + type(exception), + exception, + exception.__traceback__, + 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. @@ -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): @@ -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 @@ -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: @@ -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, diff --git a/tests/console/test_Log.py b/tests/console/test_Log.py index 2e1a650a1..d9c1e0e5e 100644 --- a/tests/console/test_Log.py +++ b/tests/console/test_Log.py @@ -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) From ab71f8b482f849e6e96aca9d640ab897d11c42f6 Mon Sep 17 00:00:00 2001 From: Tridwoxi Date: Mon, 18 May 2026 23:31:28 -0400 Subject: [PATCH 2/3] Add towncrier note for 2845 --- changes/2845.misc.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2845.misc.md diff --git a/changes/2845.misc.md b/changes/2845.misc.md new file mode 100644 index 000000000..656a5306f --- /dev/null +++ b/changes/2845.misc.md @@ -0,0 +1 @@ +Handle edge cases in the console more robustly. From 001fc0284443839bcfdb52bcfffc49f19915fdbb Mon Sep 17 00:00:00 2001 From: Darryl Wang <138794680+Tridwoxi@users.noreply.github.com> Date: Tue, 19 May 2026 23:13:53 -0400 Subject: [PATCH 3/3] Reuse known exception info in console.py --- src/briefcase/console.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/briefcase/console.py b/src/briefcase/console.py index fc05cc28a..a4d95dcca 100644 --- a/src/briefcase/console.py +++ b/src/briefcase/console.py @@ -514,17 +514,17 @@ 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. """ - _, exception, _ = sys.exc_info() - if exception is None: + exc_type, exc_value, exc_trace = sys.exc_info() + if exc_value is None: return - if isinstance(exception, BriefcaseError): - self.skip_log = exception.skip_logfile + if isinstance(exc_value, BriefcaseError): + self.skip_log = exc_value.skip_logfile trace = Traceback.extract( - type(exception), - exception, - exception.__traceback__, + exc_type, # ty:ignore[invalid-argument-type] + exc_value, + exc_trace, show_locals=True, ) self.stacktraces.append((label, trace))