adamchainz / django-rich

Extensions for using Rich with Django.
MIT License
121 stars 10 forks source link

Replace stdout and stderr with rich versions #156

Closed pydanny closed 1 year ago

pydanny commented 1 year ago

Description

I've been playing with this pattern:

class Command(RichCommand):
    def _out(self, text: str) -> None:
        self._std(text, "green")

    def _error(self, text: str) -> None:
        self._std(text, "red")

    def _std(self, text: str, color: str) -> None:
        old_text = getattr(self, "old_text", "")
        if old_text:
            self.console.log(self.old_text, style=color)
        self.status.update(f"[bold {color}]{text}")
        self.old_text = text

    def handle(self, *args, **options) -> None:
        self.stdout.write = self._out
        self.stderr.write = self._error
        with self.console.status('Starting...', spinner="bouncingBar") as self.status:
            for i in range(10):
                sleep(1)
                self.stdout.write(f"stage 1...")
            self.stderr.write(f"ERROR!")

That generates this output that looks like this:

Screenshot 2023-10-03 at 10 47 08

After some API cleanup, would this be a useful addition to django-rich? Either as an extension of RichCommand or a new class?

If so I'll put together a pull request.

adamchainz commented 1 year ago

I’m not a fan of the monkey patching of self.stdout and self.stderr. RichCommand deliberately leaves them alone and provides self.console for Rich output and making self.stderr go to stdout seems very confusing.

As to the status update pattern, it seems cleaner to me to use log directly once a “stage” is done:

from time import sleep

from django_rich.management import RichCommand

class Command(RichCommand):
    def handle(self, *args, **options):
        with self.console.status("Starting...", spinner="bouncingBar") as status:
            for i in range(1, 11):
                status.update(f"Stage {i}...")
                sleep(1)
                self.console.log(f"Stage {i} done.")

        self.console.log("All done.")

If you want a single function to update the status and log the past one, you could make it inline and pass around as necessary:

from time import sleep

from django_rich.management import RichCommand

class Command(RichCommand):
    def handle(self, *args, **options):
        with self.console.status("Starting...", spinner="bouncingBar") as status:

            def update(msg):
                self.console.log(status.status)
                status.update(msg)

            for i in range(10):
                sleep(1)
                update(f"Stage {i+1}...")

        update("Done.")

So I’m not sure there’s anything to add to django-rich from your suggestion, only a nice way of using rich.status. If we had more extensive docs I’d say we could add some usage examples there, but perhaps for now a blog post is a good idea.

pydanny commented 1 year ago

After playing a bit more with it, I understand your concern about monkeypatching.

adamchainz commented 1 year ago

I decided the pattern is worth preserving in the docs, as it’s a good example of how Rich can enhance console output beyond bold styles. Replaced the example in 1cbc1d61153d99dc37d211d772303449588aba36.