Skip to content
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

Disable spinner when using a debugger #93

Open
blairfrandeen opened this issue Jun 2, 2024 · 5 comments
Open

Disable spinner when using a debugger #93

blairfrandeen opened this issue Jun 2, 2024 · 5 comments

Comments

@blairfrandeen
Copy link

blairfrandeen commented Jun 2, 2024

When using a debugging breakpoint when a spinner is running, the text entered in the debugger is continually obscured, while the spinner remains. This means that when debugging, I need to either:

  • manually comment the spinner out of my code
  • remember to type spinner.stop() when the debugger first comes up
  • Deal with the fact that I can't see what I'm typing

Minimum code to reproduce:

import sys

from beaupy.spinners import DOTS, Spinner

spinner = Spinner(DOTS)
spinner.start()
breakpoint()

This could potentially be fixed by checking for

sys.gettrace() is not None and sys.gettrace().__name__ == "trace_dispatch"

each time the spinner cycles, and stopping the spinner if that's the case.

@petereon
Copy link
Owner

petereon commented Jun 3, 2024

Thanks for a thorough issue submission and solution suggestion. I am not friendly with pdb, so it will take me some time to set-up and verify the solution. Considering I am presently a little pressed for time, I hope I can deliver a release with a fix towards the end of the week.

@blairfrandeen
Copy link
Author

No rush, thank you! I'm also happy to take a crack at it if you prefer.

Looking at the spinner code in this library, I don't actually see anywhere to put the fix, and perhaps instead we should open an issue with rich.Live. It sounds like this was brought up there multiple times but not ever really resolved:

@petereon
Copy link
Owner

petereon commented Jun 3, 2024

Yep, having refreshed my memory on the spinners code, it seems like a rich dependency issue.

That said, I believe get_renderable callback could be extended with some sort of hack that would stop rendering of the Live display whenever we enter the debugging context, and start again once we leave it.

I'll see about setting up pdb and trying to fix this in rich. If that doesn't pan out, I'll try to hack it on top of beaupy

@petereon
Copy link
Owner

Sorry, got a little bogged down in life-stuff. This issue is not particularly easy to handle considering the layers and layers of abstraction that are required to deal with the terminal in some sort of elegant way that are included in rich.

I've researched the option of hacking it on top of beaupy as well, but I couldn't figure out a somewhat acceptable way to do it.

With that in mind, I will probably not be able to get to this in a foreseeable future 😢 Nonetheless, I'll leave the issue open in case anyone else would like to have a crack at it in a meantime.

@tzah4748
Copy link

tzah4748 commented Jul 11, 2024

Wanted to drop my hacky solution to this issue.
I didn't test this too much, but it worked for me in the simple case.

import sys
import threading
from contextlib import suppress
from dataclasses import dataclass
from threading import Thread
from time import sleep
from typing import ClassVar, Set

from rich.status import Status


@dataclass
class DebuggableStatus:
    """
    This class provides a debuggable version of the `Status` class.\\
    It automatically detects if a debugger is running and stops the status if it is, or starts the status if it is not.

    Attributes:
        - status (Status): The underlying status object.

    Example:
        ```python
        from rich.status import Status
        from p4dt_shared.rich import DebuggableStatus

        with DebuggableStatus(Status("Hello, world!")):
            breakpoint()  # This will now stop the status from updating.
        ```
    """
    _STATUSES: ClassVar[Set[Status]] = set()
    _DEBUG_DETECTION_THREAD: ClassVar[Thread] = None
    status: Status

    def __post_init__(self):
        """Adds the status to the class's list of statuses and starts the debug detection thread when required."""
        DebuggableStatus._STATUSES.add(self.status)
        if DebuggableStatus._DEBUG_DETECTION_THREAD is None or not DebuggableStatus._DEBUG_DETECTION_THREAD.is_alive():
            DebuggableStatus._DEBUG_DETECTION_THREAD = Thread(target=DebuggableStatus.detect_debug)
            DebuggableStatus._DEBUG_DETECTION_THREAD.start()

    @staticmethod
    def detect_debug():
        """
        Detects if a debugger is running and stops or starts the statuses accordingly.
        Will run indefinitely until all statuses are closed.
        """
        def is_debugger_running():
            for thread in threading.enumerate():
                if thread is threading.current_thread():
                    continue

                frame = sys._current_frames().get(thread.ident, None)
                while frame:
                    if frame.f_code.co_name == 'interaction':
                        return True
                    frame = frame.f_back

            return False

        while len(DebuggableStatus._STATUSES):
            if is_debugger_running():
                for status in DebuggableStatus._STATUSES:
                    status.stop()
            else:
                for status in DebuggableStatus._STATUSES:
                    status.start()
            sleep(1)

    def close(self):
        """Closes the status and removes it from the class's list of statuses."""
        with suppress(KeyError):
            DebuggableStatus._STATUSES.remove(self.status)

    def __enter__(self) -> Status:
        """Enters the context and returns the underlying status object."""
        return self.status.__enter__()

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Exits the context and calls the __exit__ method of the underlying status object."""
        self.close()
        return self.status.__exit__(exc_type, exc_val, exc_tb)

Usage:

console = Console()
with DebuggableStatus(console.status('Running...')):
    breakpoint()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants