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

[QUESTION] using python-rich with Typer #196

Closed
4 tasks done
Mohamed-Kaizen opened this issue Nov 23, 2020 · 11 comments
Closed
4 tasks done

[QUESTION] using python-rich with Typer #196

Mohamed-Kaizen opened this issue Nov 23, 2020 · 11 comments
Labels
question Question or problem

Comments

@Mohamed-Kaizen
Copy link

First check

  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the Typer documentation, with the integrated search.
  • I already searched in Google "How to X in Typer" and didn't find any information.
  • I already searched in Google "How to X in Click" and didn't find any information.

Description

How can I use python-rich with Typer ?

Eg.

from rich.console import Console

console = Console()

@app.command()
def version() -> None:
    """Show project Version."""
    # typer.secho(f"project Version: {__version__}", fg=typer.colors.BRIGHT_GREEN)

    console.print(f"project Version: {__version__}", style="bold green")

output:

project Version: 0.0.1

the problem that am facing is in test mode

runner = CliRunner()

def test_version_succeeds() -> None:
    """It exits with a status code of zero."""
    result = runner.invoke(app)
    assert result.exit_code == 0
    assert "project" in result.stdout

output:

FAILED tests/test_manage.py::test_version_succeeds - AssertionError: assert 'project' in ''

So is there a way to make them work together ?

@Mohamed-Kaizen Mohamed-Kaizen added the question Question or problem label Nov 23, 2020
@daddycocoaman
Copy link
Contributor

Hi,

I was just passing by this thread and I hadn't heard of Rich before seeing this question so thank you for the super dope introduction to that library.

Anyway, the Typer CliRunner is just a subclass of click's CliRunner and it doesn't appear to be the issue. In Rich, it looks like rich.print() creates a Console object and then calls console.print(). For some reason, rich.print and rich.inspect both work with CliRunner but any sort of Console object that you declare yourself doesn't. It appears that the Console object is not sending it's output in a way that CliRunner catches it.

For example, CliRunner catches rich.inspect(str) and you can see it in result.stdout_bytes. But rich.inspect(str, console=console) doesn't work and after looking through rich.print, I can't figure out what the difference between a Console object user create versus the one automatically created by the print function. Maybe it has something to do with how stdout works.

image

@Mohamed-Kaizen
Copy link
Author

Hi,
so i think i found solution but am not sure if it's good one

the eg. above say:

from rich.console import Console

console = Console()

@app.command()
def version() -> None:
    """Show project Version."""
    # typer.secho(f"project Version: {__version__}", fg=typer.colors.BRIGHT_GREEN)

    console.print(f"project Version: {__version__}", style="bold green")

this doesn't working as we know, so to make it work we need to move console = Console() inside the function in other word the code should look like this:

from rich.console import Console

@app.command()
def version() -> None:
    """Show project Version."""
    console = Console()
    # typer.secho(f"project Version: {__version__}", fg=typer.colors.BRIGHT_GREEN)

    console.print(f"project Version: {__version__}", style="bold green")

so when i run the test it's work fine but am not sure this is the best solution.

@willmcgugan
Copy link

Hi, Author of Rich here. I think what is happening is that Click is wrapping sys.stdout to catch what you print. A Console constructed at the module level will store sys.stdout prior to it being wrapped by Click.

There may be a hook in Click / Typer where you could construct a singleton Console, but I don't know enough about the API to comment.

In the meantime @Mohamed-Kaizen 's solution is a reasonable one.

@daddycocoaman
Copy link
Contributor

The solution works for getting pytest to run with simpler Typer/Click apps but it may not work for more complex apps, for example where a Typer/Click context object may have to get passed around, or any other scenario where it might be unreasonable to create a new console object for every command (like an interactive cli tool).

Ultimately the code itself works either way you write it. It's just the tests that appear to be breaking, so maybe that'll be up to the developer to figure out what works best for them.

@willmcgugan
Copy link

I made some changes to Rich recently that may impact this. Rather than store stdout when the Console is constructed, Rich will now refer to sys.stdout each time. This should mean that Rich will use the wrapped stdout from the test framework regardless of wether Console is constructed at the module scope or in a function.

@Mohamed-Kaizen
Copy link
Author

that actually work 👍 , Thanks :)

@ssbarnea
Copy link
Contributor

ssbarnea commented Apr 2, 2021

Did anyone manage to better integration rich with click/typer? Mainly I tried to add some markup to descriptions of click commands hoping that it would make help me make the --help more colors but they are rendered as plain text.

@daddycocoaman
Copy link
Contributor

Did anyone manage to better integration rich with click/typer? Mainly I tried to add some markup to descriptions of click commands hoping that it would make help me make the --help more colors but they are rendered as plain text.

@ssbarnea If you're looking to add color to your commands, use https://github.com/click-contrib/click-help-colors. It works really well.

@ssbarnea
Copy link
Contributor

ssbarnea commented Apr 6, 2021

@daddycocoaman I was able to to make use of click-help-color with typer. In case someone needs a hint on how to do it, check https://github.com/pycontribs/mk/pull/25/files

@tiangolo
Copy link
Member

Thanks for the discussion here everyone! ☕

And thanks for coming back to close the issue @Mohamed-Kaizen 🍰

For anyone coming afterwards to this, Typer now directly integrates with Rich for the help output, Rich is included and used when you install with typer[all], it's also used for error tracebacks, etc.

And Rich is also the recommended tool to display data, everywhere in the Typer docs. 🤓 🎨

Sorry for the long delay! 🙈 I wanted to personally address each issue/PR and they piled up through time, but now I'm checking each one in order.

@solesensei
Copy link

solesensei commented Oct 15, 2024

I tried from rich.logging import RichHandler from https://rich.readthedocs.io/en/stable/logging.html
And it still not working with typer CliRunner

import logging
import typer
from typer.testing import CliRunner
from rich.logging import RichHandler

app = typer.Typer()
runner = CliRunner()
logging.basicConfig(
    level="NOTSET",
    format="%(message)s",
    datefmt="%X",
    handlers=[RichHandler()],
)

logger = logging.getLogger(__name__)


@app.command()
def logging_command() -> None:
    """Show project Version."""
    typer.echo("Typer Message")
    logger.info("Rich logging message")


def test_logging_rich() -> None:
    """It exits with a status code of zero."""
    result = runner.invoke(app)
    assert result.exit_code == 0
    assert "Typer Message" in result.stdout  # True
    assert "Rich logging message" in result.stdout  # AssertionError

Maybe I set something up wrong?

UPD. It looks like I need to set the level on the logger itself.

logger = logging.getLogger(__name__)
handler = RichHandler()
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)

Now it works 🎉

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

No branches or pull requests

6 participants