-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
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