Skip to content

Override Result Formatting for pretty and non-intrusive CLIs 🌈✨ #344

@beasteers

Description

@beasteers

Fire's main benefit is that it can be dropped in and turn anything into a CLI! But sometimes there are certain types that either don't play nice with fire's formatters and some that just don't print out in a pretty way.

What I'm proposing: Allow users to provide a function that can override result formatting. Simply, it takes the output of the fire call and passes it through the user formatter, and then the result that call is treated as the result of the original fire call.

Outputting tabular data:

import random
def data():
    return [
        {
            'is_up': random.choice([False, True]), 
            'value_A': random.random(), 
            'value_B': random.random() + 100,
            'id': random.randint(1000, 5000)} 
        for i in range(8)
    ]

import fire
fire.Fire(data)

Outputs this:

{"is_up": true, "value_A": 0.6004291859538904, "value_B": 100.77910907893889, "id": 474}
{"is_up": false, "value_A": 0.1406617230697117, "value_B": 100.35721554966845, "id": 740}
{"is_up": true, "value_A": 0.3612392830626744, "value_B": 100.60814663568802, "id": 509}
{"is_up": false, "value_A": 0.11247653550250092, "value_B": 100.2673181440675, "id": 305}
{"is_up": false, "value_A": 0.9505598630828166, "value_B": 100.84615141986525, "id": 85}
{"is_up": true, "value_A": 0.17544933002396768, "value_B": 100.66062056951291, "id": 385}
{"is_up": false, "value_A": 0.25245927587860695, "value_B": 100.75492369068093, "id": 923}
{"is_up": true, "value_A": 0.9237200249249168, "value_B": 100.94228120845642, "id": 702}

But if we can define a formatter.

import tabulate

def fancy_table(result):
    if not result:  # show a message instead of an empty response
        return 'nada. sorry.'
    
    # display a list of dicts as a table
    if isinstance(result, (list, tuple)) and all(isinstance(x, dict) for x in result):
        return tabulate.tabulate([
            {col: cell_format(value) for col, value in row.items()}
            for row in result
        ], headers="keys")

    return result  # otherwise, let fire handle it

def cell_format(value, decimals=3, bool=('🌹', 'πŸ₯€')):
    if value is True:
        return bool[0]
    if value is False:
        return bool[1]
    if isinstance(value, float):
        return '{:.{}f}'.format(value, decimals)
    return value

fire.Fire(data, formatter=fancy_table)

Outputs this:

is_up      value_A    value_B    id
-------  ---------  ---------  ----
🌹           0.115    100.013  1821
🌹           0.439    100.167  4242
πŸ₯€           0.68     100.345  2937
πŸ₯€           0.074    100.119  4675
🌹           0.189    100.462  4571
🌹           0.221    100.342  1522
🌹           0.02     100.452  2363
πŸ₯€           0.023    100.812  2433

And to address the potential comment that you can do this already by wrapping the function: yes that's true for simple cases, but if you start wrapping classes or a family of functions (fire.Fire() can call any defined function) it becomes less realistic and then fire needs wrappers for all of those functions which makes it much more cumbersome (I'm literally doing this in a project and it's such a pain). And that doesn't get into how much more difficult it'd be to wrap object methods that return other objects with their own methods, etc. And on top of that, wrappers mess with function signature inspection which is used by fire for command line help strings so it's problematic in many ways.

This is a very simple addition to fire that can supercharge the look of your CLI with very little effort and intervention in existing code!

Real Use Cases that I've personally encountered:

  • Rest API class (A class that wraps a collection of API methods): provide API result data in pretty ways (e.g. like the table above!!). Right now I'm doing it manually by subclassing it and wrapping every single method (UGH!)
  • Keras models: they do not print out nicely when returned from Fire and I have a file full of functions that return them. It would be a big hassle to wrap them all when I just want to test a couple out quickly

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions