Repository: Textualize/rich

Issue:
[BUG] Panel docstring says the "box" parameter is optional, when it is not
- [x] I've checked [docs](https://rich.readthedocs.io/en/latest/introduction.html) and [closed issues](https://github.com/Textualize/rich/issues?q=is%3Aissue+is%3Aclosed) for possible solutions.
- [x] I can't find my issue in the [FAQ](https://github.com/Textualize/rich/blob/master/FAQ.md).
- [x] I also checked open issues and didn't see anything like this

**Describe the bug**

Printing a Panel with `box=None` raises an `AttributeError: 'NoneType' object has no attribute 'substitute'`

```
import rich.console
import rich.panel

c = rich.console.Console()
c.print(rich.panel.Panel("Hello there."))
c.print(rich.panel.Panel("General Kenobi.", box=rich.box.DOUBLE))
c.print(rich.panel.Panel("You are a bold one.", box=None))
```
![Image](https://github.com/user-attachments/assets/9c4bf9ef-1503-4615-9dd0-44d853352a3f)


**Platform**
<details>
<summary>Click to expand</summary>

What platform (Win/Linux/Mac) are you running on? What terminal software are you using?

MacOS 15.2 on Mac Studio with Apple M1 Max. Terminal software is iTerm. Python version 3.13.1.

I may ask you to copy and paste the output of the following commands. It may save some time if you do it now.

If you're using Rich in a terminal:

```
python -m rich.diagnose
pip freeze | grep rich
```

```
╭───────────────────────── <class 'rich.console.Console'> ─────────────────────────╮
│ A high level console interface.                                                  │
│                                                                                  │
│ ╭──────────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=100 ColorSystem.TRUECOLOR>                                    │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                  │
│     color_system = 'truecolor'                                                   │
│         encoding = 'utf-8'                                                       │
│             file = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> │
│           height = 25                                                            │
│    is_alt_screen = False                                                         │
│ is_dumb_terminal = False                                                         │
│   is_interactive = True                                                          │
│       is_jupyter = False                                                         │
│      is_terminal = True                                                          │
│   legacy_windows = False                                                         │
│         no_color = False                                                         │
│          options = ConsoleOptions(                                               │
│                        size=ConsoleDimensions(width=100, height=25),             │
│                        legacy_windows=False,                                     │
│                        min_width=1,                                              │
│                        max_width=100,                                            │
│                        is_terminal=True,                                         │
│                        encoding='utf-8',                                         │
│                        max_height=25,                                            │
│                        justify=None,                                             │
│                        overflow=None,                                            │
│                        no_wrap=False,                                            │
│                        highlight=None,                                           │
│                        markup=None,                                              │
│                        height=None                                               │
│                    )                                                             │
│            quiet = False                                                         │
│           record = False                                                         │
│         safe_box = True                                                          │
│             size = ConsoleDimensions(width=100, height=25)                       │
│        soft_wrap = False                                                         │
│           stderr = False                                                         │
│            style = None                                                          │
│         tab_size = 8                                                             │
│            width = 100                                                           │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭─── <class 'rich._windows.WindowsConsoleFeatures'> ────╮
│ Windows features available.                           │
│                                                       │
│ ╭───────────────────────────────────────────────────╮ │
│ │ WindowsConsoleFeatures(vt=False, truecolor=False) │ │
│ ╰───────────────────────────────────────────────────╯ │
│                                                       │
│ truecolor = False                                     │
│        vt = False                                     │
╰───────────────────────────────────────────────────────╯
╭────── Environment Variables ───────╮
│ {                                  │
│     'TERM': 'xterm-256color',      │
│     'COLORTERM': 'truecolor',      │
│     'CLICOLOR': None,              │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': 'iTerm.app',   │
│     'COLUMNS': None,               │
│     'LINES': None,                 │
│     'JUPYTER_COLUMNS': None,       │
│     'JUPYTER_LINES': None,         │
│     'JPY_PARENT_PID': None,        │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
╰────────────────────────────────────╯
platform="Darwin"
rich==13.9.4
rich-argparse==1.6.0
```


</details>

Discussion:
@willmcgugan: ~~The box parameter doesn't accept `None`, which you can see from the [reference](https://rich.readthedocs.io/en/latest/reference/panel.html).~~

@willmcgugan: Ah, the docstring is in error. The `box` isn't optional, contrary to the docstring.@kotfu: Gotcha. I'm happy to make a PR.@github-actions: I hope we solved your problem.

If you like using Rich, you might also enjoy [Textual](https://textual.textualize.io)

Solution patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 37db4643..b9425011 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -20,7 +20,6 @@ repos:
   - repo: https://github.com/pre-commit/pygrep-hooks
     rev: v1.10.0
     hooks:
-      - id: python-check-mock-methods
       - id: python-no-log-warn
       - id: python-use-type-annotations
       - id: rst-directive-colons
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c7332fe..b84fce7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## Unreleased
+
+### Added
+
+- Added env var `TTY_COMPATIBLE` to override auto-detection of TTY support (See console.rst for details). https://github.com/Textualize/rich/pull/3675
+
+### Changed
+
+- An empty `NO_COLOR` env var is now considered disabled. https://github.com/Textualize/rich/pull/3675
+- An empty `FORCE_COLOR` env var is now considered disabled. https://github.com/Textualize/rich/pull/3675
+- Rich tracebacks will now render notes on Python 3.11 onwards (added with `Exception.add_note`) https://github.com/Textualize/rich/pull/3676
+- Indentation in exceptions won't be underlined https://github.com/Textualize/rich/pull/3678
+- Rich tracebacks will now render Exception Groups https://github.com/Textualize/rich/pull/3677
 
 ## [13.9.4] - 2024-11-01
 
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index d8985ca1..9893915a 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -91,3 +91,4 @@ The following people have contributed to the development of Rich:
 - [L. Yeung](https://github.com/lewis-yeung)
 - [chthollyphile](https://github.com/chthollyphile)
 - [Jonathan Helmus](https://github.com/jjhelmus)
+- [Brandon Capener](https://github.com/bcapener)
diff --git a/docs/source/console.rst b/docs/source/console.rst
index 5c0aeb32..c0d50ca1 100644
--- a/docs/source/console.rst
+++ b/docs/source/console.rst
@@ -420,11 +420,18 @@ Rich respects some standard environment variables.
 
 Setting the environment variable ``TERM`` to ``"dumb"`` or ``"unknown"`` will disable color/style and some features that require moving the cursor, such as progress bars.
 
-If the environment variable ``FORCE_COLOR`` is set, then color/styles will be enabled regardless of the value of ``TERM``. This is useful on CI systems which aren't terminals but can none-the-less display ANSI escape sequences.
+If the environment variable ``FORCE_COLOR`` is set and non-empty, then color/styles will be enabled regardless of the value of ``TERM``.
 
-If the environment variable ``NO_COLOR`` is set, Rich will disable all color in the output. This takes precedence over ``FORCE_COLOR``. See `no_color <https://no-color.org/>`_ for details.
+If the environment variable ``NO_COLOR`` is set, Rich will disable all color in the output. ``NO_COLOR`` takes precedence over ``FORCE_COLOR``. See `no_color <https://no-color.org/>`_ for details.
 
 .. note::
     The ``NO_COLOR`` environment variable removes *color* only. Styles such as dim, bold, italic, underline etc. are preserved.
 
-If ``width`` / ``height`` arguments are not explicitly provided as arguments to ``Console`` then the environment variables ``COLUMNS``/``LINES`` can be used to set the console width/height. ``JUPYTER_COLUMNS``/``JUPYTER_LINES`` behave similarly and are used in Jupyter.
+The environment variable ``TTY_COMPATIBLE`` is used to override Rich's auto-detection of terminal support. If ``TTY_COMPATIBLE`` is set to ``1`` then Rich will assume it is writing to a device which can handle escape sequences like a terminal. If ``TTY_COMPATIBLE`` is set to ``"0"``, then Rich will assume that it is writing to a device that is *not* capable of displaying escape sequences (such as a regular file). If the variable is not set, or set to a value other than "0" or "1", then Rich will attempt to auto-detect terminal support.
+
+.. note::
+    If you want Rich output in CI or Github Actions, then you should set ``TTY_COMPATIBLE=1``.
+
+If ``width`` / ``height`` arguments are not explicitly provided as arguments to ``Console`` then the environment variables ``COLUMNS`` / ``LINES`` can be used to set the console width / height. ``JUPYTER_COLUMNS`` / ``JUPYTER_LINES`` behave similarly and are used in Jupyter.
+
+Note that environment variables set defaults in the Console object. If you explicitly set any variables in the constructor then these will take precedence.
diff --git a/docs/source/style.rst b/docs/source/style.rst
index 21dd6def..4d5e4df8 100644
--- a/docs/source/style.rst
+++ b/docs/source/style.rst
@@ -108,7 +108,7 @@ Style Themes
 
 If you reuse styles it can be a maintenance headache if you ever want to modify an attribute or color -- you would have to change every line where the style is used. Rich provides a :class:`~rich.theme.Theme` class which you can use to define custom styles that you can refer to by name. That way you only need to update your styles in one place.
 
-Style themes can make your code more semantic, for instance a style called ``"warning"`` better expresses intent that ``"italic magenta underline"``.
+Style themes can make your code more semantic, for instance a style called ``"warning"`` better expresses intent than ``"italic magenta underline"``.
 
 To use a style theme, construct a :class:`~rich.theme.Theme` instance and pass it to the :class:`~rich.console.Console` constructor. Here's an example::
 
diff --git a/rich/console.py b/rich/console.py
index 3ec9a8aa..6725e405 100644
--- a/rich/console.py
+++ b/rich/console.py
@@ -500,7 +500,7 @@ def group(fit: bool = True) -> Callable[..., Callable[..., Group]]:
     """
 
     def decorator(
-        method: Callable[..., Iterable[RenderableType]]
+        method: Callable[..., Iterable[RenderableType]],
     ) -> Callable[..., Group]:
         """Convert a method that returns an iterable of renderables in to a Group."""
 
@@ -735,7 +735,9 @@ class Console:
         self.get_time = get_time or monotonic
         self.style = style
         self.no_color = (
-            no_color if no_color is not None else "NO_COLOR" in self._environ
+            no_color
+            if no_color is not None
+            else self._environ.get("NO_COLOR", "") != ""
         )
         self.is_interactive = (
             (self.is_terminal and not self.is_dumb_terminal)
@@ -933,11 +935,13 @@ class Console:
 
         Returns:
             bool: True if the console writing to a device capable of
-            understanding terminal codes, otherwise False.
+                understanding escape sequences, otherwise False.
         """
+        # If dev has explicitly set this value, return it
         if self._force_terminal is not None:
             return self._force_terminal
 
+        # Fudge for Idle
         if hasattr(sys.stdin, "__module__") and sys.stdin.__module__.startswith(
             "idlelib"
         ):
@@ -948,12 +952,22 @@ class Console:
             # return False for Jupyter, which may have FORCE_COLOR set
             return False
 
-        # If FORCE_COLOR env var has any value at all, we assume a terminal.
-        force_color = self._environ.get("FORCE_COLOR")
-        if force_color is not None:
-            self._force_terminal = True
+        environ = self._environ
+
+        tty_compatible = environ.get("TTY_COMPATIBLE", "")
+        # 0 indicates device is not tty compatible
+        if tty_compatible == "0":
+            return False
+        # 1 indicates device is tty compatible
+        if tty_compatible == "1":
             return True
 
+        # https://force-color.org/
+        force_color = environ.get("FORCE_COLOR")
+        if force_color is not None:
+            return force_color != ""
+
+        # Any other value defaults to auto detect
         isatty: Optional[Callable[[], bool]] = getattr(self.file, "isatty", None)
         try:
             return False if isatty is None else isatty()
diff --git a/rich/default_styles.py b/rich/default_styles.py
index 28e8f6f9..3a0ad83a 100644
--- a/rich/default_styles.py
+++ b/rich/default_styles.py
@@ -120,7 +120,9 @@ DEFAULT_STYLES: Dict[str, Style] = {
     "traceback.exc_type": Style(color="bright_red", bold=True),
     "traceback.exc_value": Style.null(),
     "traceback.offset": Style(color="bright_red", bold=True),
-    "traceback.error_range": Style(underline=True, bold=True, dim=False),
+    "traceback.error_range": Style(underline=True, bold=True),
+    "traceback.note": Style(color="green", bold=True),
+    "traceback.group.border": Style(color="magenta"),
     "bar.back": Style(color="grey23"),
     "bar.complete": Style(color="rgb(249,38,114)"),
     "bar.finished": Style(color="rgb(114,156,31)"),
diff --git a/rich/diagnose.py b/rich/diagnose.py
index 91e55bd8..5d163877 100644
--- a/rich/diagnose.py
+++ b/rich/diagnose.py
@@ -15,16 +15,17 @@ def report() -> None:  # pragma: no cover
     inspect(features)
 
     env_names = (
-        "TERM",
-        "COLORTERM",
         "CLICOLOR",
-        "NO_COLOR",
-        "TERM_PROGRAM",
+        "COLORTERM",
         "COLUMNS",
-        "LINES",
+        "JPY_PARENT_PID",
         "JUPYTER_COLUMNS",
         "JUPYTER_LINES",
-        "JPY_PARENT_PID",
+        "LINES",
+        "NO_COLOR",
+        "TERM_PROGRAM",
+        "TERM",
+        "TTY_COMPATIBLE",
         "VSCODE_VERBOSE_LOGGING",
     )
     env = {name: os.getenv(name) for name in env_names}
diff --git a/rich/panel.py b/rich/panel.py
index 8cfa6f4a..d411e291 100644
--- a/rich/panel.py
+++ b/rich/panel.py
@@ -22,7 +22,7 @@ class Panel(JupyterMixin):
 
     Args:
         renderable (RenderableType): A console renderable object.
-        box (Box, optional): A Box instance that defines the look of the border (see :ref:`appendix_box`. Defaults to box.ROUNDED.
+        box (Box): A Box instance that defines the look of the border (see :ref:`appendix_box`. Defaults to box.ROUNDED.
         title (Optional[TextType], optional): Optional title displayed in panel header. Defaults to None.
         title_align (AlignMethod, optional): Alignment of title. Defaults to "center".
         subtitle (Optional[TextType], optional): Optional subtitle displayed in panel footer. Defaults to None.
diff --git a/rich/style.py b/rich/style.py
index 262fd6ec..835d06f3 100644
--- a/rich/style.py
+++ b/rich/style.py
@@ -524,7 +524,7 @@ class Style:
                 if not word:
                     raise errors.StyleSyntaxError("color expected after 'on'")
                 try:
-                    Color.parse(word) is None
+                    Color.parse(word)
                 except ColorParseError as error:
                     raise errors.StyleSyntaxError(
                         f"unable to parse {word!r} as background color; {error}"
diff --git a/rich/table.py b/rich/table.py
index 7b1fa9e4..942175dc 100644
--- a/rich/table.py
+++ b/rich/table.py
@@ -929,7 +929,6 @@ class Table(JupyterMixin):
 if __name__ == "__main__":  # pragma: no cover
     from rich.console import Console
     from rich.highlighter import ReprHighlighter
-    from rich.table import Table as Table
 
     from ._timer import timer
 
diff --git a/rich/traceback.py b/rich/traceback.py
index 3bf5baa7..b2cc6304 100644
--- a/rich/traceback.py
+++ b/rich/traceback.py
@@ -26,15 +26,22 @@ from pygments.token import Token
 from pygments.util import ClassNotFound
 
 from . import pretty
-from ._loop import loop_last
+from ._loop import loop_first_last, loop_last
 from .columns import Columns
-from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group
+from .console import (
+    Console,
+    ConsoleOptions,
+    ConsoleRenderable,
+    Group,
+    RenderResult,
+    group,
+)
 from .constrain import Constrain
 from .highlighter import RegexHighlighter, ReprHighlighter
 from .panel import Panel
 from .scope import render_scope
 from .style import Style
-from .syntax import Syntax
+from .syntax import Syntax, SyntaxPosition
 from .text import Text
 from .theme import Theme
 
@@ -44,6 +51,34 @@ LOCALS_MAX_LENGTH = 10
 LOCALS_MAX_STRING = 80
 
 
+def _iter_syntax_lines(
+    start: SyntaxPosition, end: SyntaxPosition
+) -> Iterable[Tuple[int, int, int]]:
+    """Yield start and end positions per line.
+
+    Args:
+        start: Start position.
+        end: End position.
+
+    Returns:
+        Iterable of (LINE, COLUMN1, COLUMN2).
+    """
+
+    line1, column1 = start
+    line2, column2 = end
+
+    if line1 == line2:
+        yield line1, column1, column2
+    else:
+        for first, last, line_no in loop_first_last(range(line1, line2 + 1)):
+            if first:
+                yield line_no, column1, -1
+            elif last:
+                yield line_no, 0, column2
+            else:
+                yield line_no, 0, -1
+
+
 def install(
     *,
     console: Optional[Console] = None,
@@ -100,26 +135,25 @@ def install(
         value: BaseException,
         traceback: Optional[TracebackType],
     ) -> None:
-        traceback_console.print(
-            Traceback.from_exception(
-                type_,
-                value,
-                traceback,
-                width=width,
-                code_width=code_width,
-                extra_lines=extra_lines,
-                theme=theme,
-                word_wrap=word_wrap,
-                show_locals=show_locals,
-                locals_max_length=locals_max_length,
-                locals_max_string=locals_max_string,
-                locals_hide_dunder=locals_hide_dunder,
-                locals_hide_sunder=bool(locals_hide_sunder),
-                indent_guides=indent_guides,
-                suppress=suppress,
-                max_frames=max_frames,
-            )
+        exception_traceback = Traceback.from_exception(
+            type_,
+            value,
+            traceback,
+            width=width,
+            code_width=code_width,
+            extra_lines=extra_lines,
+            theme=theme,
+            word_wrap=word_wrap,
+            show_locals=show_locals,
+            locals_max_length=locals_max_length,
+            locals_max_string=locals_max_string,
+            locals_hide_dunder=locals_hide_dunder,
+            locals_hide_sunder=bool(locals_hide_sunder),
+            indent_guides=indent_guides,
+            suppress=suppress,
+            max_frames=max_frames,
         )
+        traceback_console.print(exception_traceback)
 
     def ipy_excepthook_closure(ip: Any) -> None:  # pragma: no cover
         tb_data = {}  # store information about showtraceback call
@@ -191,6 +225,7 @@ class _SyntaxError:
     line: str
     lineno: int
     msg: str
+    notes: List[str] = field(default_factory=list)
 
 
 @dataclass
@@ -200,6 +235,9 @@ class Stack:
     syntax_error: Optional[_SyntaxError] = None
     is_cause: bool = False
     frames: List[Frame] = field(default_factory=list)
+    notes: List[str] = field(default_factory=list)
+    is_group: bool = False
+    exceptions: List["Trace"] = field(default_factory=list)
 
 
 @dataclass
@@ -403,6 +441,8 @@ class Traceback:
 
         from rich import _IMPORT_CWD
 
+        notes: List[str] = getattr(exc_value, "__notes__", None) or []
+
         def safe_str(_object: Any) -> str:
             """Don't allow exceptions from __str__ to propagate."""
             try:
@@ -415,8 +455,25 @@ class Traceback:
                 exc_type=safe_str(exc_type.__name__),
                 exc_value=safe_str(exc_value),
                 is_cause=is_cause,
+                notes=notes,
             )
 
+            if sys.version_info >= (3, 11):
+                if isinstance(exc_value, (BaseExceptionGroup, ExceptionGroup)):
+                    stack.is_group = True
+                    for exception in exc_value.exceptions:
+                        stack.exceptions.append(
+                            Traceback.extract(
+                                type(exception),
+                                exception,
+                                exception.__traceback__,
+                                show_locals=show_locals,
+                                locals_max_length=locals_max_length,
+                                locals_hide_dunder=locals_hide_dunder,
+                                locals_hide_sunder=locals_hide_sunder,
+                            )
+                        )
+
             if isinstance(exc_value, SyntaxError):
                 stack.syntax_error = _SyntaxError(
                     offset=exc_value.offset or 0,
@@ -424,13 +481,14 @@ class Traceback:
                     lineno=exc_value.lineno or 0,
                     line=exc_value.text or "",
                     msg=exc_value.msg,
+                    notes=notes,
                 )
 
             stacks.append(stack)
             append = stack.frames.append
 
             def get_locals(
-                iter_locals: Iterable[Tuple[str, object]]
+                iter_locals: Iterable[Tuple[str, object]],
             ) -> Iterable[Tuple[str, object]]:
                 """Extract locals from an iterator of key pairs."""
                 if not (locals_hide_dunder or locals_hide_sunder):
@@ -524,6 +582,7 @@ class Traceback:
             break  # pragma: no cover
 
         trace = Trace(stacks=stacks)
+
         return trace
 
     def __rich_console__(
@@ -556,7 +615,9 @@ class Traceback:
         )
 
         highlighter = ReprHighlighter()
-        for last, stack in loop_last(reversed(self.trace.stacks)):
+
+        @group()
+        def render_stack(stack: Stack, last: bool) -> RenderResult:
             if stack.frames:
                 stack_renderable: ConsoleRenderable = Panel(
                     self._render_stack(stack),
@@ -569,6 +630,7 @@ class Traceback:
                 stack_renderable = Constrain(stack_renderable, self.width)
                 with console.use_theme(traceback_theme):
                     yield stack_renderable
+
             if stack.syntax_error is not None:
                 with console.use_theme(traceback_theme):
                     yield Constrain(
@@ -594,6 +656,24 @@ class Traceback:
             else:
                 yield Text.assemble((f"{stack.exc_type}", "traceback.exc_type"))
 
+            for note in stack.notes:
+                yield Text.assemble(("[NOTE] ", "traceback.note"), highlighter(note))
+
+            if stack.is_group:
+                for group_no, group_exception in enumerate(stack.exceptions, 1):
+                    grouped_exceptions: List[Group] = []
+                    for group_last, group_stack in loop_last(group_exception.stacks):
+                        grouped_exceptions.append(render_stack(group_stack, group_last))
+                    yield ""
+                    yield Constrain(
+                        Panel(
+                            Group(*grouped_exceptions),
+                            title=f"Sub-exception #{group_no}",
+                            border_style="traceback.group.border",
+                        ),
+                        self.width,
+                    )
+
             if not last:
                 if stack.is_cause:
                     yield Text.from_markup(
@@ -604,6 +684,9 @@ class Traceback:
                         "\n[i]During handling of the above exception, another exception occurred:\n",
                     )
 
+        for last, stack in loop_last(reversed(self.trace.stacks)):
+            yield render_stack(stack, last)
+
     @group()
     def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult:
         highlighter = ReprHighlighter()
@@ -648,17 +731,6 @@ class Traceback:
         path_highlighter = PathHighlighter()
         theme = self.theme
 
-        def read_code(filename: str) -> str:
-            """Read files, and cache results on filename.
-
-            Args:
-                filename (str): Filename to read
-
-            Returns:
-                str: Contents of file
-            """
-            return "".join(linecache.getlines(filename))
-
         def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
             if frame.locals:
                 yield render_scope(
@@ -720,7 +792,8 @@ class Traceback:
                 continue
             if not suppressed:
                 try:
-                    code = read_code(frame.filename)
+                    code_lines = linecache.getlines(frame.filename)
+                    code = "".join(code_lines)
                     if not code:
                         # code may be an empty string if the file doesn't exist, OR
                         # if the traceback filename is generated dynamically
@@ -749,12 +822,26 @@ class Traceback:
                 else:
                     if frame.last_instruction is not None:
                         start, end = frame.last_instruction
-                        syntax.stylize_range(
-                            style="traceback.error_range",
-                            start=start,
-                            end=end,
-                            style_before=True,
-                        )
+
+                        # Stylize a line at a time
+                        # So that indentation isn't underlined (which looks bad)
+                        for line1, column1, column2 in _iter_syntax_lines(start, end):
+                            try:
+                                if column1 == 0:
+                                    line = code_lines[line1 - 1]
+                                    column1 = len(line) - len(line.lstrip())
+                                if column2 == -1:
+                                    column2 = len(code_lines[line1 - 1])
+                            except IndexError:
+                                # Being defensive here
+                                # If last_instruction reports a line out-of-bounds, we don't want to crash
+                                continue
+
+                            syntax.stylize_range(
+                                style="traceback.error_range",
+                                start=(line1, column1),
+                                end=(line1, column2),
+                            )
                     yield (
                         Columns(
                             [


Test patch:
diff --git a/tests/test_console.py b/tests/test_console.py
index 407a138e..206dbdd1 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -34,7 +34,7 @@ from rich.text import Text
 os.get_terminal_size
 
 
-def test_dumb_terminal():
+def test_dumb_terminal() -> None:
     console = Console(force_terminal=True, _environ={})
     assert console.color_system is not None
 
@@ -45,14 +45,14 @@ def test_dumb_terminal():
     assert height == 25
 
 
-def test_soft_wrap():
+def test_soft_wrap() -> None:
     console = Console(file=io.StringIO(), width=20, soft_wrap=True)
     console.print("foo " * 10)
     assert console.file.getvalue() == "foo " * 20
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
-def test_16color_terminal():
+def test_16color_terminal() -> None:
     console = Console(
         force_terminal=True, _environ={"TERM": "xterm-16color"}, legacy_windows=False
     )
@@ -60,7 +60,7 @@ def test_16color_terminal():
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
-def test_truecolor_terminal():
+def test_truecolor_terminal() -> None:
     console = Console(
         force_terminal=True,
         legacy_windows=False,
@@ -70,7 +70,7 @@ def test_truecolor_terminal():
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
-def test_kitty_terminal():
+def test_kitty_terminal() -> None:
     console = Console(
         force_terminal=True,
         legacy_windows=False,
@@ -79,7 +79,7 @@ def test_kitty_terminal():
     assert console.color_system == "256"
 
 
-def test_console_options_update():
+def test_console_options_update() -> None:
     options = ConsoleOptions(
         ConsoleDimensions(80, 25),
         max_height=25,
@@ -103,7 +103,7 @@ def test_console_options_update():
     assert options_copy == options and options_copy is not options
 
 
-def test_console_options_update_height():
+def test_console_options_update_height() -> None:
     options = ConsoleOptions(
         ConsoleDimensions(80, 25),
         max_height=25,
@@ -120,7 +120,7 @@ def test_console_options_update_height():
     assert render_options.max_height == 12
 
 
-def test_init():
+def test_init() -> None:
     console = Console(color_system=None)
     assert console._color_system == None
     console = Console(color_system="standard")
@@ -128,7 +128,7 @@ def test_init():
     console = Console(color_system="auto")
 
 
-def test_size():
+def test_size() -> None:
     console = Console()
     w, h = console.size
     assert console.width == w
@@ -180,37 +180,37 @@ def test_size_can_fall_back_to_std_descriptors(
     assert (w, h) == expected_size
 
 
-def test_repr():
+def test_repr() -> None:
     console = Console()
     assert isinstance(repr(console), str)
     assert isinstance(str(console), str)
 
 
-def test_print():
+def test_print() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print("foo")
     assert console.file.getvalue() == "foo\n"
 
 
-def test_print_multiple():
+def test_print_multiple() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print("foo", "bar")
     assert console.file.getvalue() == "foo bar\n"
 
 
-def test_print_text():
+def test_print_text() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print(Text("foo", style="bold"))
-    assert console.file.getvalue() == "\x1B[1mfoo\x1B[0m\n"
+    assert console.file.getvalue() == "\x1b[1mfoo\x1b[0m\n"
 
 
-def test_print_text_multiple():
+def test_print_text_multiple() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print(Text("foo", style="bold"), Text("bar"), "baz")
-    assert console.file.getvalue() == "\x1B[1mfoo\x1B[0m bar baz\n"
+    assert console.file.getvalue() == "\x1b[1mfoo\x1b[0m bar baz\n"
 
 
-def test_print_json():
+def test_print_json() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print_json('[false, true, null, "foo"]', indent=4)
     result = console.file.getvalue()
@@ -219,13 +219,13 @@ def test_print_json():
     assert result == expected
 
 
-def test_print_json_error():
+def test_print_json_error() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     with pytest.raises(TypeError):
         console.print_json(["foo"], indent=4)
 
 
-def test_print_json_data():
+def test_print_json_data() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print_json(data=[False, True, None, "foo"], indent=4)
     result = console.file.getvalue()
@@ -234,7 +234,7 @@ def test_print_json_data():
     assert result == expected
 
 
-def test_print_json_ensure_ascii():
+def test_print_json_ensure_ascii() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print_json(data={"foo": "💩"}, ensure_ascii=False)
     result = console.file.getvalue()
@@ -243,7 +243,7 @@ def test_print_json_ensure_ascii():
     assert result == expected
 
 
-def test_print_json_with_default_ensure_ascii():
+def test_print_json_with_default_ensure_ascii() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print_json(data={"foo": "💩"})
     result = console.file.getvalue()
@@ -252,7 +252,7 @@ def test_print_json_with_default_ensure_ascii():
     assert result == expected
 
 
-def test_print_json_indent_none():
+def test_print_json_indent_none() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     data = {"name": "apple", "count": 1}
     console.print_json(data=data, indent=None)
@@ -261,7 +261,7 @@ def test_print_json_indent_none():
     assert result == expected
 
 
-def test_console_null_file(monkeypatch):
+def test_console_null_file(monkeypatch) -> None:
     # When stdout and stderr are null, Console.file should be replaced with NullFile
     monkeypatch.setattr("sys.stdout", None)
     monkeypatch.setattr("sys.stderr", None)
@@ -270,7 +270,7 @@ def test_console_null_file(monkeypatch):
     assert isinstance(console.file, NullFile)
 
 
-def test_log():
+def test_log() -> None:
     console = Console(
         file=io.StringIO(),
         width=80,
@@ -286,7 +286,7 @@ def test_log():
     assert result == expected
 
 
-def test_log_milliseconds():
+def test_log_milliseconds() -> None:
     def time_formatter(timestamp: datetime) -> Text:
         return Text("TIME")
 
@@ -298,13 +298,13 @@ def test_log_milliseconds():
     assert result == "TIME foo                                \n"
 
 
-def test_print_empty():
+def test_print_empty() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print()
     assert console.file.getvalue() == "\n"
 
 
-def test_markup_highlight():
+def test_markup_highlight() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print("'[bold]foo[/bold]'")
     assert (
@@ -313,13 +313,13 @@ def test_markup_highlight():
     )
 
 
-def test_print_style():
+def test_print_style() -> None:
     console = Console(file=io.StringIO(), color_system="truecolor")
     console.print("foo", style="bold")
     assert console.file.getvalue() == "\x1b[1mfoo\x1b[0m\n"
 
 
-def test_show_cursor():
+def test_show_cursor() -> None:
     console = Console(
         file=io.StringIO(), force_terminal=True, legacy_windows=False, _environ={}
     )
@@ -329,31 +329,31 @@ def test_show_cursor():
     assert console.file.getvalue() == "\x1b[?25lfoo\n\x1b[?25h"
 
 
-def test_clear():
+def test_clear() -> None:
     console = Console(file=io.StringIO(), force_terminal=True, _environ={})
     console.clear()
     console.clear(home=False)
     assert console.file.getvalue() == "\033[2J\033[H" + "\033[2J"
 
 
-def test_clear_no_terminal():
+def test_clear_no_terminal() -> None:
     console = Console(file=io.StringIO())
     console.clear()
     console.clear(home=False)
     assert console.file.getvalue() == ""
 
 
-def test_get_style():
+def test_get_style() -> None:
     console = Console()
     console.get_style("repr.brace") == Style(bold=True)
 
 
-def test_get_style_default():
+def test_get_style_default() -> None:
     console = Console()
     console.get_style("foobar", default="red") == Style(color="red")
 
 
-def test_get_style_error():
+def test_get_style_error() -> None:
     console = Console()
     with pytest.raises(errors.MissingStyle):
         console.get_style("nosuchstyle")
@@ -361,20 +361,20 @@ def test_get_style_error():
         console.get_style("foo bar")
 
 
-def test_render_error():
+def test_render_error() -> None:
     console = Console()
     with pytest.raises(errors.NotRenderableError):
         list(console.render([], console.options))
 
 
-def test_control():
+def test_control() -> None:
     console = Console(file=io.StringIO(), force_terminal=True, _environ={})
     console.control(Control.clear())
     console.print("BAR")
     assert console.file.getvalue() == "\x1b[2JBAR\n"
 
 
-def test_capture():
+def test_capture() -> None:
     console = Console()
     with console.capture() as capture:
         with pytest.raises(CaptureError):
@@ -383,7 +383,7 @@ def test_capture():
     assert capture.get() == "Hello\n"
 
 
-def test_input(monkeypatch, capsys):
+def test_input(monkeypatch, capsys) -> None:
     def fake_input(prompt=""):
         console.file.write(prompt)
         return "bar"
@@ -395,7 +395,7 @@ def test_input(monkeypatch, capsys):
     assert user_input == "bar"
 
 
-def test_input_password(monkeypatch, capsys):
+def test_input_password(monkeypatch, capsys) -> None:
     def fake_input(prompt, stream=None):
         console.file.write(prompt)
         return "bar"
@@ -409,37 +409,37 @@ def test_input_password(monkeypatch, capsys):
     assert user_input == "bar"
 
 
-def test_status():
+def test_status() -> None:
     console = Console(file=io.StringIO(), force_terminal=True, width=20)
     status = console.status("foo")
     assert isinstance(status, Status)
 
 
-def test_justify_none():
+def test_justify_none() -> None:
     console = Console(file=io.StringIO(), force_terminal=True, width=20)
     console.print("FOO", justify=None)
     assert console.file.getvalue() == "FOO\n"
 
 
-def test_justify_left():
+def test_justify_left() -> None:
     console = Console(file=io.StringIO(), force_terminal=True, width=20, _environ={})
     console.print("FOO", justify="left")
     assert console.file.getvalue() == "FOO                 \n"
 
 
-def test_justify_center():
+def test_justify_center() -> None:
     console = Console(file=io.StringIO(), force_terminal=True, width=20, _environ={})
     console.print("FOO", justify="center")
     assert console.file.getvalue() == "        FOO         \n"
 
 
-def test_justify_right():
+def test_justify_right() -> None:
     console = Console(file=io.StringIO(), force_terminal=True, width=20, _environ={})
     console.print("FOO", justify="right")
     assert console.file.getvalue() == "                 FOO\n"
 
 
-def test_justify_renderable_none():
+def test_justify_renderable_none() -> None:
     console = Console(
         file=io.StringIO(),
         force_terminal=True,
@@ -451,7 +451,7 @@ def test_justify_renderable_none():
     assert console.file.getvalue() == "╭───╮\n│FOO│\n╰───╯\n"
 
 
-def test_justify_renderable_left():
+def test_justify_renderable_left() -> None:
     console = Console(
         file=io.StringIO(),
         force_terminal=True,
@@ -463,7 +463,7 @@ def test_justify_renderable_left():
     assert console.file.getvalue() == "╭───╮     \n│FOO│     \n╰───╯     \n"
 
 
-def test_justify_renderable_center():
+def test_justify_renderable_center() -> None:
     console = Console(
         file=io.StringIO(),
         force_terminal=True,
@@ -475,7 +475,7 @@ def test_justify_renderable_center():
     assert console.file.getvalue() == "  ╭───╮   \n  │FOO│   \n  ╰───╯   \n"
 
 
-def test_justify_renderable_right():
+def test_justify_renderable_right() -> None:
     console = Console(
         file=io.StringIO(),
         force_terminal=True,
@@ -495,14 +495,14 @@ class BrokenRenderable:
         pass
 
 
-def test_render_broken_renderable():
+def test_render_broken_renderable() -> None:
     console = Console()
     broken = BrokenRenderable()
     with pytest.raises(errors.NotRenderableError):
         list(console.render(broken, console.options))
 
 
-def test_export_text():
+def test_export_text() -> None:
     console = Console(record=True, width=100)
     console.print("[b]foo")
     text = console.export_text()
@@ -510,7 +510,7 @@ def test_export_text():
     assert text == expected
 
 
-def test_export_html():
+def test_export_html() -> None:
     console = Console(record=True, width=100)
     console.print("[b]foo <script> 'test' [link=https://example.org]Click[/link]")
     html = console.export_html()
@@ -519,7 +519,7 @@ def test_export_html():
     assert html == expected
 
 
-def test_export_html_inline():
+def test_export_html_inline() -> None:
     console = Console(record=True, width=100)
     console.print("[b]foo [link=https://example.org]Click[/link]")
     html = console.export_html(inline_styles=True)
@@ -531,7 +531,7 @@ def test_export_html_inline():
 EXPECTED_SVG = '<svg class="rich-terminal" viewBox="0 0 1238 74.4" xmlns="http://www.w3.org/2000/svg">\n    <!-- Generated with Rich https://www.textualize.io -->\n    <style>\n\n    @font-face {\n        font-family: "Fira Code";\n        src: local("FiraCode-Regular"),\n                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),\n                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");\n        font-style: normal;\n        font-weight: 400;\n    }\n    @font-face {\n        font-family: "Fira Code";\n        src: local("FiraCode-Bold"),\n                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),\n                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");\n        font-style: bold;\n        font-weight: 700;\n    }\n\n    .terminal-3526644552-matrix {\n        font-family: Fira Code, monospace;\n        font-size: 20px;\n        line-height: 24.4px;\n        font-variant-east-asian: full-width;\n    }\n\n    .terminal-3526644552-title {\n        font-size: 18px;\n        font-weight: bold;\n        font-family: arial;\n    }\n\n    .terminal-3526644552-r1 { fill: #608ab1;font-weight: bold }\n.terminal-3526644552-r2 { fill: #c5c8c6 }\n    </style>\n\n    <defs>\n    <clipPath id="terminal-3526644552-clip-terminal">\n      <rect x="0" y="0" width="1219.0" height="23.4" />\n    </clipPath>\n    \n    </defs>\n\n    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="72.4" rx="8"/><text class="terminal-3526644552-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">Rich</text>\n            <g transform="translate(26,22)">\n            <circle cx="0" cy="0" r="7" fill="#ff5f57"/>\n            <circle cx="22" cy="0" r="7" fill="#febc2e"/>\n            <circle cx="44" cy="0" r="7" fill="#28c840"/>\n            </g>\n        \n    <g transform="translate(9, 41)" clip-path="url(#terminal-3526644552-clip-terminal)">\n    <rect fill="#cc555a" x="0" y="1.5" width="36.6" height="24.65" shape-rendering="crispEdges"/>\n    <g class="terminal-3526644552-matrix">\n    <text class="terminal-3526644552-r1" x="0" y="20" textLength="36.6" clip-path="url(#terminal-3526644552-line-0)">foo</text><text class="terminal-3526644552-r2" x="48.8" y="20" textLength="61" clip-path="url(#terminal-3526644552-line-0)">Click</text><text class="terminal-3526644552-r2" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-3526644552-line-0)">\n</text>\n    </g>\n    </g>\n</svg>\n'
 
 
-def test_export_svg():
+def test_export_svg() -> None:
     console = Console(record=True, width=100)
     console.print(
         "[b red on blue reverse]foo[/] [blink][link=https://example.org]Click[/link]"
@@ -542,7 +542,7 @@ def test_export_svg():
     assert svg == EXPECTED_SVG
 
 
-def test_export_svg_specified_unique_id():
+def test_export_svg_specified_unique_id() -> None:
     expected_svg = EXPECTED_SVG.replace("terminal-3526644552", "given-id")
     console = Console(record=True, width=100)
     console.print(
@@ -554,7 +554,7 @@ def test_export_svg_specified_unique_id():
     assert svg == expected_svg
 
 
-def test_save_svg():
+def test_save_svg() -> None:
     console = Console(record=True, width=100)
     console.print(
         "[b red on blue reverse]foo[/] [blink][link=https://example.org]Click[/link]"
@@ -566,7 +566,7 @@ def test_save_svg():
             assert svg_file.read() == EXPECTED_SVG
 
 
-def test_save_text():
+def test_save_text() -> None:
     console = Console(record=True, width=100)
     console.print("foo")
     with tempfile.TemporaryDirectory() as path:
@@ -576,7 +576,7 @@ def test_save_text():
             assert text_file.read() == "foo\n"
 
 
-def test_save_html():
+def test_save_html() -> None:
     expected = '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<style>\n\nbody {\n    color: #000000;\n    background-color: #ffffff;\n}\n</style>\n</head>\n<body>\n    <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><code style="font-family:inherit">foo\n</code></pre>\n</body>\n</html>\n'
     console = Console(record=True, width=100)
     console.print("foo")
@@ -589,13 +589,13 @@ def test_save_html():
             assert html == expected
 
 
-def test_no_wrap():
+def test_no_wrap() -> None:
     console = Console(width=10, file=io.StringIO())
     console.print("foo bar baz egg", no_wrap=True)
     assert console.file.getvalue() == "foo bar ba\n"
 
 
-def test_soft_wrap():
+def test_soft_wrap() -> None:
     console = Console(width=10, file=io.StringIO())
     console.print("foo bar baz egg", soft_wrap=True)
     assert console.file.getvalue() == "foo bar baz egg\n"
@@ -696,7 +696,7 @@ def test_console_style() -> None:
     assert result == expected
 
 
-def test_no_color():
+def test_no_color() -> None:
     console = Console(
         file=io.StringIO(), color_system="truecolor", force_terminal=True, no_color=True
     )
@@ -707,13 +707,13 @@ def test_no_color():
     assert result == expected
 
 
-def test_quiet():
+def test_quiet() -> None:
     console = Console(file=io.StringIO(), quiet=True)
     console.print("Hello, World!")
     assert console.file.getvalue() == ""
 
 
-def test_no_nested_live():
+def test_no_nested_live() -> None:
     console = Console()
     with pytest.raises(errors.LiveError):
         with console.status("foo"):
@@ -722,7 +722,7 @@ def test_no_nested_live():
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
-def test_screen():
+def test_screen() -> None:
     console = Console(
         color_system=None, force_terminal=True, force_interactive=True, _environ={}
     )
@@ -736,7 +736,7 @@ def test_screen():
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
-def test_screen_update():
+def test_screen_update() -> None:
     console = Console(
         width=20, height=4, color_system="truecolor", force_terminal=True, _environ={}
     )
@@ -751,12 +751,12 @@ def test_screen_update():
     assert result == expected
 
 
-def test_height():
+def test_height() -> None:
     console = Console(width=80, height=46)
     assert console.height == 46
 
 
-def test_columns_env():
+def test_columns_env() -> None:
     console = Console(_environ={"COLUMNS": "314"}, legacy_windows=False)
     assert console.width == 314
     # width take precedence
@@ -766,7 +766,7 @@ def test_columns_env():
     console = Console(width=40, _environ={"COLUMNS": "broken"}, legacy_windows=False)
 
 
-def test_lines_env():
+def test_lines_env() -> None:
     console = Console(_environ={"LINES": "220"})
     assert console.height == 220
     # height take precedence
@@ -776,7 +776,7 @@ def test_lines_env():
     console = Console(width=40, _environ={"LINES": "broken"})
 
 
-def test_screen_update_class():
+def test_screen_update_class() -> None:
     screen_update = ScreenUpdate([[Segment("foo")], [Segment("bar")]], 5, 10)
     assert screen_update.x == 5
     assert screen_update.y == 10
@@ -790,7 +790,7 @@ def test_screen_update_class():
     assert result == expected
 
 
-def test_is_alt_screen():
+def test_is_alt_screen() -> None:
     console = Console(force_terminal=True)
     if console.legacy_windows:
         return
@@ -800,7 +800,7 @@ def test_is_alt_screen():
     assert not console.is_alt_screen
 
 
-def test_set_console_title():
+def test_set_console_title() -> None:
     console = Console(force_terminal=True, _environ={})
     if console.legacy_windows:
         return
@@ -812,7 +812,7 @@ def test_set_console_title():
     assert result == "\x1b]0;hello\x07"
 
 
-def test_update_screen():
+def test_update_screen() -> None:
     console = Console(force_terminal=True, width=20, height=5, _environ={})
     if console.legacy_windows:
         return
@@ -828,7 +828,7 @@ def test_update_screen():
     assert result == expected
 
 
-def test_update_screen_lines():
+def test_update_screen_lines() -> None:
     console = Console(force_terminal=True, width=20, height=5)
     if console.legacy_windows:
         return
@@ -836,21 +836,21 @@ def test_update_screen_lines():
         console.update_screen_lines([])
 
 
-def test_update_options_markup():
+def test_update_options_markup() -> None:
     console = Console()
     options = console.options
     assert options.update(markup=False).markup == False
     assert options.update(markup=True).markup == True
 
 
-def test_print_width_zero():
+def test_print_width_zero() -> None:
     console = Console()
     with console.capture() as capture:
         console.print("Hello", width=0)
     assert capture.get() == ""
 
 
-def test_size_properties():
+def test_size_properties() -> None:
     console = Console(width=80, height=25, legacy_windows=False)
     assert console.size == ConsoleDimensions(80, 25)
     console.size = (10, 20)
@@ -861,7 +861,7 @@ def test_size_properties():
     assert console.size == ConsoleDimensions(5, 10)
 
 
-def test_print_newline_start():
+def test_print_newline_start() -> None:
     console = Console(width=80, height=25)
     console.begin_capture()
     console.print("Foo", new_line_start=True)
@@ -871,7 +871,7 @@ def test_print_newline_start():
     assert result == "Foo\n\nFoo\nbar\n\n"
 
 
-def test_is_terminal_broken_file():
+def test_is_terminal_broken_file() -> None:
     console = Console()
 
     def _mock_isatty():
@@ -883,12 +883,12 @@ def test_is_terminal_broken_file():
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="not relevant on Windows")
-def test_detect_color_system():
+def test_detect_color_system() -> None:
     console = Console(_environ={"TERM": "rxvt-unicode-256color"}, force_terminal=True)
     assert console._detect_color_system() == ColorSystem.EIGHT_BIT
 
 
-def test_reset_height():
+def test_reset_height() -> None:
     """Test height is reset when rendering complex renderables."""
 
     # https://github.com/Textualize/rich/issues/2042
@@ -914,7 +914,7 @@ def test_reset_height():
     assert result == expected
 
 
-def test_render_lines_height_minus_vertical_pad_is_negative():
+def test_render_lines_height_minus_vertical_pad_is_negative() -> None:
     # https://github.com/Textualize/textual/issues/389
     console = Console(
         force_terminal=True,
@@ -929,7 +929,7 @@ def test_render_lines_height_minus_vertical_pad_is_negative():
     console.render_lines(Padding("hello", pad=(1, 0)), options=options)
 
 
-def test_recording_no_stdout_and_no_stderr_files(monkeypatch):
+def test_recording_no_stdout_and_no_stderr_files(monkeypatch) -> None:
     # Rich should work even if there's no file available to write to.
     # For example, pythonw nullifies output streams.
     # Built-in print silently no-ops in pythonw.
@@ -942,7 +942,7 @@ def test_recording_no_stdout_and_no_stderr_files(monkeypatch):
     assert text == "hello world\n"
 
 
-def test_capturing_no_stdout_and_no_stderr_files(monkeypatch):
+def test_capturing_no_stdout_and_no_stderr_files(monkeypatch) -> None:
     monkeypatch.setattr("sys.stdout", None)
     monkeypatch.setattr("sys.stderr", None)
     console = Console()
@@ -952,14 +952,14 @@ def test_capturing_no_stdout_and_no_stderr_files(monkeypatch):
 
 
 @pytest.mark.parametrize("env_value", ["", "something", "0"])
-def test_force_color(env_value):
+def test_force_color(env_value) -> None:
     # Even though we use a non-tty file, the presence of FORCE_COLOR env var
     # means is_terminal returns True.
     console = Console(file=io.StringIO(), _environ={"FORCE_COLOR": env_value})
     assert console.is_terminal
 
 
-def test_force_color_jupyter():
+def test_force_color_jupyter() -> None:
     # FORCE_COLOR above doesn't happen in a Jupyter kernel
     console = Console(
         file=io.StringIO(), _environ={"FORCE_COLOR": "1"}, force_jupyter=True
@@ -967,7 +967,7 @@ def test_force_color_jupyter():
     assert not console.is_terminal
 
 
-def test_force_color():
+def test_force_color() -> None:
     console = Console(
         file=io.StringIO(),
         _environ={
@@ -1044,3 +1044,69 @@ def test_capture_and_record() -> None:
     recorded_content = console.export_text()
     print(repr(recorded_content))
     assert recorded_content == "Print 0\n"
+
+
+def test_tty_compatible() -> None:
+    """Check TTY_COMPATIBLE environment var."""
+
+    class FakeTTY:
+        """An file file-like which reports it is a TTY."""
+
+        def __init__(self) -> None:
+            self.called_isatty = False
+
+        def isatty(self) -> bool:
+            self.called_isatty = True
+            return True
+
+    class FakeFile:
+        """A file object that reports False for isatty"""
+
+        def __init__(self) -> None:
+            self.called_isatty = False
+
+        def isatty(self) -> bool:
+            self.called_isatty = True
+            return False
+
+    # Console file is not a TTY
+    console = Console(file=FakeFile())
+    # Not a TTY, so is_terminal should be False
+    assert not console.is_terminal
+    # Should have called isatty to auto-detect tty support
+    assert console.file.called_isatty
+
+    # Not a terminal
+    console = Console(file=FakeFile(), _environ={"TTY_COMPATIBLE": "1"})
+    # env TTY_COMPATIBLE=1 should report that it is a terminal
+    assert console.is_terminal
+    # Should not have called file.isattry
+    assert not console.file.called_isatty
+
+    # File is a fake TTY
+    console = Console(file=FakeTTY())
+    # Should report True
+    assert console.is_terminal
+    # SHould have auto-detected
+    assert console.file.called_isatty
+
+    # File is a fake TTY
+    console = Console(file=FakeTTY(), _environ={"TTY_COMPATIBLE": ""})
+    # Blank TTY_COMPATIBLE should auto-detect, so is_terminal is True
+    assert console.is_terminal
+    # Should have auto-detected
+    assert console.file.called_isatty
+
+    # File is a fake TTY
+    console = Console(file=FakeTTY(), _environ={"TTY_COMPATIBLE": "whatever"})
+    # Any pother value should auto-detect
+    assert console.is_terminal
+    # Should have auto-detected
+    assert console.file.called_isatty
+
+    # TTY_COMPATIBLE should override file.isattry
+    console = Console(file=FakeTTY(), _environ={"TTY_COMPATIBLE": "0"})
+    # Should report that it is *not* a terminal
+    assert not console.is_terminal
+    # Should not have auto-detected
+    assert not console.file.called_isatty
diff --git a/tests/test_traceback.py b/tests/test_traceback.py
index ed80d1ba..bc9bc91e 100644
--- a/tests/test_traceback.py
+++ b/tests/test_traceback.py
@@ -358,3 +358,18 @@ def test_traceback_finely_grained() -> None:
         start, end = last_instruction
         print(start, end)
         assert start[0] == end[0]
+
+
+@pytest.mark.skipif(
+    sys.version_info.minor < 11, reason="Not supported before Python 3.11"
+)
+def test_notes() -> None:
+    """Check traceback captures __note__."""
+    try:
+        1 / 0
+    except Exception as error:
+        error.add_note("Hello")
+        error.add_note("World")
+        traceback = Traceback()
+
+        assert traceback.trace.stacks[0].notes == ["Hello", "World"]