Interacting with user

Everything about yuio.io and Yuio’s interactive capabilities.

Printing messages

Yuio offers a logging-like functions to print messages:

import yuio.app
import yuio.io

@yuio.app.app
def main():
    yuio.io.heading("Message colors")

    yuio.io.success("Success message is bold green")
    yuio.io.failure("Failure message is bold red")
    yuio.io.info("Info message is default color")
    yuio.io.warning("Warning message is yellow")
    yuio.io.error("Error message is red")

    yuio.io.info("Messages can have `%r`", ["formatted", "content"])

if __name__ == "__main__":
    main.run()
import yuio.app
import yuio.io

@yuio.app.app
def main():
    yuio.io.heading("Message colors")

    yuio.io.success("Success message is bold green")
    yuio.io.failure("Failure message is bold red")
    yuio.io.info("Info message is default color")
    yuio.io.warning("Warning message is yellow")
    yuio.io.error("Error message is red")

    yuio.io.info(t"Messages can have `{['formatted', 'content']!r}`")

if __name__ == "__main__":
    main.run()

These functions accept format strings that can have color tags and backticks:

yuio.io.info(
    "Color tags are similar to XML: "
    "<c bold green>this text is bold and green</c>"  # [1]_
)

yuio.io.info(
    "Backticks work like in Markdown: "
    "`this is an escaped code, tags like <c red> don't work here.`"
)

yuio.io.info(
    "You can escape backticks and other punctuation: "
    "\\`\\<c red> this is normal text \\</c>\\`."
)

value = "this string contains <c red>color tags</c>"
yuio.io.info(
    "Interpolated values aren't processed: %s",
    value,
)
  1. See full list of tags in yuio.theme.

yuio.io.info(
    "Color tags are similar to XML: "
    "<c bold green>this text is bold and green</c>"  # [1]_
)

yuio.io.info(
    "Backticks work like in Markdown: "
    "`this is an escaped code, tags like <c red> don't work here.`"
)

yuio.io.info(
    "You can escape backticks and other punctuation: "
    "\\`\\<c red> this is normal text \\</c>\\`."
)

value = "this string contains <c red>color tags</c>"
yuio.io.info(
    t"Interpolated values aren't processed: {value}",
)
  1. See full list of tags in yuio.theme.

Pretty-printing Python objects

Yuio supports rich repr protocol, which enables you to pretty-print Python objects. Pretty-printing is controlled through format flags for %r and %s, as well as through formatting flags for template strings:

  • # enables colors in repr (i.e. %#r);

  • + splits repr into multiple lines (i.e. %+r, %#+r).

import dataclasses
import yuio.app
import yuio.io

@dataclasses.dataclass
class CoordinateSystem:
    origin: tuple[int, int] = (0, 0)
    scale: tuple[float, float] = (1.0, 1.0)

@yuio.app.app
def main():
    coordinates = CoordinateSystem()
    yuio.io.info("Main coordinate system: %#+r", coordinates)

if __name__ == "__main__":
    main.run()
  • # enables colors in repr (i.e. {var:#});

  • + splits repr into multiple lines (i.e. {var:+}, {var:#+});

  • these flags work when you format strings or colorable objects: you’ll have to explicitly convert objects of other types to strings by specifying conversion operator, i.e. {var!r:#}.

import dataclasses
import yuio.app
import yuio.io

@dataclasses.dataclass
class CoordinateSystem:
    origin: tuple[int, int] = (0, 0)
    scale: tuple[float, float] = (1.0, 1.0)

@yuio.app.app
def main():
    coordinates = CoordinateSystem()
    yuio.io.info(t"Main coordinate system: {coordinates!r:#+}")

if __name__ == "__main__":
    main.run()

", ".join(map(repr, values))

You often need to print lists joined by some separator. Yuio provides JoinStr, JoinRepr, And, and Or to help with this task:

ALLOWED_VALUES = ["foo", "bar", "baz"]

yuio.io.info(
    "Allowed values: %s",
    yuio.io.JoinRepr(ALLOWED_VALUES, color="magenta"),  # [1]_
)
  1. You can pass multiple color tags in the same string, just separate them with spaces.

ALLOWED_VALUES = ["foo", "bar", "baz"]

yuio.io.info(
    t"Allowed values: {yuio.io.JoinRepr(ALLOWED_VALUES, color='magenta')}",  # [1]_
)
  1. You can pass multiple color tags in the same string, just separate them with spaces.

RST and Markdown

Yuio also supports basic RST and Markdown formatting. It’s mostly used for generating CLI help, but you can print messages with it as well:

yuio.io.rst("""
    Greetings!
    ----------

    -   You can use *inline formatting*, ``backticks``,
        and :py:class:`interpreted text <yuio.md.MdParser>`.
    -   Hyperlinks `also work`__!
    -   You can also use some common directives, though not all of them:

        .. warning::

            Oh, and tables are not supported, at least for now.

    -   Plus, there's syntax highlighting. For example, check out this fork bomb:

        .. code-block:: sh

            :(){ :|:& };:  # <- don't paste this in bash!

    __ https://yuio.readthedocs.io/
""")
yuio.io.md("""
    # Greetings!

    -   You can use *inline formatting*, `backticks`,
        and {py:class}`MySt roles <yuio.md.MdParser>`.
    -   Hyperlinks [also work]!
    -   You can also use CommonMark block markup and MyST directives:

        ```{warning}
        Tables are not supported, though.
        ```
    -   Plus, there's syntax highlighting. For example, check out this fork bomb:

        ```sh
        :(){ :|:& };:  # <- don't paste this in bash!
        ```

    [also work]: https://yuio.readthedocs.io/
""")

Highlighting code

Yuio supports simple code highlighting:

yuio.io.hl(
    """
    {
        "version": "1.0.0",
        "pre-release": false,
        "post-release": false,
    }
    """,
    syntax="json",  # [1]_
)
  1. See full list of supported languages in yuio.hl.

Querying user input

You can use yuio.io.ask() to get data from the user. It’s like input(), but automatically parses the user input, and can use different widgets based on the expected value’s type:

import enum
import yuio.app
import yuio.io

class GreetingType(enum.Enum):  # [1]_
    FORMAL = "Formal"
    INFORMAL = "Informal"

@yuio.app.app
def main():
    name = yuio.io.ask("What's your name?")
    greeting_type = yuio.io.ask[GreetingType](  # [2]_
        "What kind of greeting do you want?",
        default=GreetingType.FORMAL,
    )

  1. We use Enum so that Yuio knows which values to expect. It will change input widget accordingly.

  2. ask() accepts type parameter which determines its result. Default is str.

Note

yuio.io.ask() is designed to interact with users, not to read data. It uses /dev/tty on Unix, and console API on Windows, so it will read from an actual TTY even if stdin is redirected.

When designing your program, make sure that users have alternative means to provide values: use configs or CLI arguments, allow passing passwords via environment variables, etc.

Indicating progress

Suppose you have some long-running job, and you want to indicate that it is running. yuio.io.Task to the rescue:

with yuio.io.Task("Sending a greeting"):
    send_greeting()

And if the job can report its progress, we can even show a progressbar:

with yuio.io.Task("Sending greetings") as task:
    for i, email in enumerate(emails):
        task.comment(email)
        task.progress(i, len(emails))  # [1]_

        send_greeting()
  1. Task has lots of helper methods on it.

    For example, this code can be simplified using Task.iter() instead of enumerate().

Opening an external editor

You know how when you run git commit, it opens an editor and asks you to edit a commit message? Yuio can do the same:

greeting = yuio.io.edit(
    "# Please, edit the greeting:\n"
    "Hello, world!",
    comment_marker="#",  # [1]_
)

yuio.io.info("Greeting: `%s`", greeting)
  1. All lines that start with this marker will be removed after editing.

Logging

The app will automatically perform a basic logging configuration on startup. The logging level will depend on how many -v flags are given, from WARNING to DEBUG:

import logging
import yuio.app

logger = logging.getLogger("main")

@yuio.app.app
def main(foo: str = "", bar: str = ""):
    logger.debug("settings: foo=%r, bar=%r", foo, bar)
    logger.info("Running some logic...")

if __name__ == "__main__":
    main.run()

This is how verbose output will look like:

If you want to configure logging yourself, set yuio.app.App.setup_logging to False, and use yuio.io.Handler to send logs to stderr:

import logging
import yuio.app
import yuio.io

logger = logging.getLogger("main")

@yuio.app.app
def main(foo: str = "", bar: str = ""):
    logging.basicConfig(
        level=logging.INFO,
        handlers=[yuio.io.Handler()],
    )

    logger.debug("settings: foo=%r, bar=%r", foo, bar)
    logger.info("Running some logic...")

main.setup_logging = False

if __name__ == "__main__":
    main.run()

Suspending output

Sometimes you need to stop all output from your program. Most often this happens when you want to hand IO control to a subprocess. yuio.io.SuspendOutput does just that:

with yuio.io.SuspendOutput():
    subprocess.check_call(["git", "status"])

Tip

yuio.exec provides a simple wrapper around subprocess that will log process’ stderr, and return process’ stdout.

yuio.io.SuspendOutput will disable all output, including prints and writes to sys.stderr and sys.stdout. To bypass it, use yuio.io.orig_stderr(), yuio.io.orig_stdout(), and methods on the yuio.io.SuspendOutput class.

Here’s a more comprehensive example:

import time
import subprocess
import yuio.app
import yuio.io

@yuio.app.app
def main():
    with yuio.io.Task("Performing some task"):
        time.sleep(1)

        # All progress bars, prints, and so on are suspended
        # inside of this context manager.
        with yuio.io.SuspendOutput() as o:
            # But you can manually bypass output suspension.
            o.info("Running `git status`:")

            subprocess.check_call(["git", "status"])

        time.sleep(1)