Skip to content

[PR #13462/cba5c4a9 backport][8.4.x] fix: support TerminalReporter.isatty being called #13483

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
3 changes: 3 additions & 0 deletions changelog/13461.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Corrected ``_pytest.terminal.TerminalReporter.isatty`` to support
being called as a method. Before it was just a boolean which could
break correct code when using ``-o log_cli=true``).
20 changes: 20 additions & 0 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,23 @@ def get_user_id() -> int | None:
# This also work for Enums (if you use `is` to compare) and Literals.
def assert_never(value: NoReturn) -> NoReturn:
assert False, f"Unhandled value: {value} ({type(value).__name__})"


class CallableBool:
"""
A bool-like object that can also be called, returning its true/false value.

Used for backwards compatibility in cases where something was supposed to be a method
but was implemented as a simple attribute by mistake (see `TerminalReporter.isatty`).

Do not use in new code.
"""

def __init__(self, value: bool) -> None:
self._value = value

def __bool__(self) -> bool:
return self._value

def __call__(self) -> bool:
return self._value
11 changes: 7 additions & 4 deletions src/_pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

import pluggy

from _pytest import compat
from _pytest import nodes
from _pytest import timing
from _pytest._code import ExceptionInfo
Expand Down Expand Up @@ -387,7 +388,9 @@ def __init__(self, config: Config, file: TextIO | None = None) -> None:
self.reportchars = getreportopt(config)
self.foldskipped = config.option.fold_skipped
self.hasmarkup = self._tw.hasmarkup
self.isatty = file.isatty()
# isatty should be a method but was wrongly implemented as a boolean.
# We use CallableBool here to support both.
self.isatty = compat.CallableBool(file.isatty())
self._progress_nodeids_reported: set[str] = set()
self._timing_nodeids_reported: set[str] = set()
self._show_progress_info = self._determine_show_progress_info()
Expand Down Expand Up @@ -766,7 +769,7 @@ def _width_of_current_line(self) -> int:
return self._tw.width_of_current_line

def pytest_collection(self) -> None:
if self.isatty:
if self.isatty():
if self.config.option.verbose >= 0:
self.write("collecting ... ", flush=True, bold=True)
elif self.config.option.verbose >= 1:
Expand All @@ -779,7 +782,7 @@ def pytest_collectreport(self, report: CollectReport) -> None:
self._add_stats("skipped", [report])
items = [x for x in report.result if isinstance(x, Item)]
self._numcollected += len(items)
if self.isatty:
if self.isatty():
self.report_collect()

def report_collect(self, final: bool = False) -> None:
Expand Down Expand Up @@ -811,7 +814,7 @@ def report_collect(self, final: bool = False) -> None:
line += f" / {skipped} skipped"
if self._numcollected > selected:
line += f" / {selected} selected"
if self.isatty:
if self.isatty():
self.rewrite(line, bold=True, erase=True)
if final:
self.write("\n")
Expand Down
10 changes: 10 additions & 0 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,16 @@ def test_long_xfail():
]
)

@pytest.mark.parametrize("isatty", [True, False])
def test_isatty(self, pytester: Pytester, monkeypatch, isatty: bool) -> None:
config = pytester.parseconfig()
f = StringIO()
monkeypatch.setattr(f, "isatty", lambda: isatty)
tr = TerminalReporter(config, f)
assert tr.isatty() == isatty
# It was incorrectly implemented as a boolean so we still support using it as one.
assert bool(tr.isatty) == isatty


class TestCollectonly:
def test_collectonly_basic(self, pytester: Pytester) -> None:
Expand Down