-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Add custom formatter for Fire result #345
Conversation
can't agree more, as mentioned here, after each iteration of parsing, the component is updated to another new object, so there is no way to uniformly process the final result (maybe we need to remove some keys in the final dict or beautify the final result). actually, i think it might be better to let users to provide a customized print method instead of a formatter. At present, |
Glad other people find this useful too! That is an interesting idea. I still like the idea of a formatter because it allows you to do a minimal override and pre-process the message if you want to, but still rely on the built in output mechanisms for everything else - so if you override it for one type, builtin formatting for everything else will still work as normal. def my_formatter(x):
# handle a class that doesn't have a nice string output
if isinstance(x, tf.keras.Model): # like a keras model
x.summary() # prints a nice output
return # return empty so the default object rendering doesn't happen
return x # everything else is handled normally, e.g. dicts, lists, objects But what you're talking about is possible with the formatter argument in this PR. You can just print from inside the formatter function and then return def my_formatter(x):
print(x, end=' ') # print instead of return and you can add print arguments |
aha, here # The command succeeded normally; print the result.
_PrintResult(component_trace, verbose=component_trace.verbose)
result = component_trace.GetResult()
return result after: # The command succeeded normally; print the result.
result = component_trace.GetResult()
# show result
if has_customized_printer():
customized_printer(result, component_trace.verbose)
else:
# here we can separate the helper display from `_PrintResult`, let it keep the simple display function.
_PrintResultWithoutHepler(result, verbose=component_trace.verbose)
# show helper
if should_show_help():
show_help(component_trace, component_trace.verbose)
return result Anyway, it's just a little bit of my personal thought, and I think your unifed formatter is already great idea, hope to merged ASAP |
Hmm not sure if I understand what your suggestion is 🤔 Ya the custom But to clarify - in 95% percent of cases, you will not need to print inside of a formatter function, you just return the result. If you want it to print out a string, you can just return a string. But if you need to control how something is printed, you are free to do it using the way described above. Yeah I hope it gets merged soon too! Thanks for your input! |
Yes, this is a welcome change! Have a look at #188 (comment) Summary of top-level thoughts:
|
yep no worries! I probably wouldn't be able to work on it until then anyways either.
love it, sounds much better ✨
I've never used interactive mode so I'll need to dig in to see what this means, but the display function doesn't replace the result value used in chaining. So regardless of what the display function returns, the fire chaining will still work the same way because it still uses the original value. (the format call happens inside the _PrintResult call which has no return value). (let me know if I misunderstood haha)
Hmm maybe we could have |
Wait actually reading thru that other thread more, this PR is actually providing def json_format(x):
return json.dumps(x, indent=4)
fire.Fire(..., format=json_format) as well as csv, yaml, (fancy_table is also just a serialization) etc... Like this was never meant as a display function that needs to manage printing out anything (though you are free to do it as a side effect if you want) It's primarily just to provide serialization overrides, with the ability to fallback to default serialization implicitly (by returning something other than None/str). Also with this method, if you want to combine multiple formatters that work for different types, all you'd have to do is: fire.Fire(..., format=lambda x: (
list_of_dicts_as_table(
list_of_lists_as_csv(
dicts_as_yaml(x)))
)) which should work out of the box currently (provided you define those formatters). But this composition would not be possible without the pass-through mechanism. |
Yes! That's exactly the direction I was thinking in: The user can specify either (or both) of two arguments (as you saw, I'm thinking to call them The rough signature of serialize is For your use-case, you could in principle use either serialize or display, but like you say I think serialize is more appropriate. If you use serialize, the recommended approach would be to return the table as a string. If you use display, then you would have to print the string yourself rather than returning it. |
Ok cool! In that case I don't think there's much more to do other than rename formatter= to serialize= and maybe add a couple builtin serializers. Idk if we should do display= as a separate PR? I'm less familiar with the nuances around output and paging that ppl are requesting. (Obvs we can decide that down the road when we both have time) |
Yes, adding them in separate PRs would be fine. (Edit: And it goes without saying you don't need to do both of them. Thank you for your contribution so far. If you want to help out adding more, that's wonderful and welcome, but of course entirely up to you. :)) |
sounds good! yeah I'll probably let someone more motivated by the display overriding handle it since I don't have much of an opinion about how it should be dealt with. Cool this is exciting tho! |
# Allow users to modify the return value of the component and provide | ||
# custom formatting. | ||
if callable(serialize): | ||
result = serialize(result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll want to handle the not-callable case too.
I think if we cannot call serialize, we raise a FireError.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we just do:
if serialize:
result = serialize(result)
which will just raise whatever error the object would raise if tried to be called (probably typeerror)
And this would allow any falsey value to disable serialize (None, False, etc)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed - it will now raise FireError("serialize argument {} must be empty or callable.".format(serialize))
if isinstance(x, dict): | ||
return ', '.join('{}={!r}'.format(k, v) for k, v in x.items()) | ||
if x == 'special': | ||
return ['SURPRISE!!', "I'm a list!"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name "serialize" implies that the output of serialize is either bytes or a string.
But that's not what we're requiring.
I wonder if there's a better name (maybe that's "format" after all.)
I'll think on this, but curious for your thoughts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe "process" or "finalize"...
I'll keep thinking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forgive my unpolished thinking aloud:
Maybe "serialize" is fine, and the logic is: "if you don't finish serializing it, we'll continue serializing it for you using the default serializer."
And then "display" will only ever be given bytes or a string, rather than being directly given the output of serialize.
One challenge with this is that it disallows different serialization for different display modes.
We could (in a subsequent PR) allow serialize to accept the "kind" parameter too to reenable this capability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I mean I personally like format
too, but serialize
can also work and I think that rationale is fine
One challenge with this is that it disallows different serialization for different display modes
I'm not sure I understand the motivation behind this. Do you mean that you'd want the display mode to have its own default serializer to fall back on? If that's the case, then maybe there could be another mode of serializer override tied to the display object.
# could also be defined as a class if you want
def mydisplay(x):
print(x)
def serialize(x):
return x
mydisplay.serialize = serialize
# then internally _Fire(..., display=mydisplay)
default_serialize = getattr(display, 'serialize', None)
if default_serialize:
result = default_serialize(result)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
side note: one thing that would be nice is if we could have some sort of override via the CLI where the user could do --format json
or --format csv
or something like that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some notes about the naming - I like serialize
and format
because they both imply a string result. I think there might be a problem with process
or finalize
cuz they could imply that you have the ability to modify the result after each iteration (after each getattr / getitem / call).
I can't think of any other words better than those two tho.
Maybe "serialize" is fine, and the logic is: "if you don't finish serializing it, we'll continue serializing it for you using the default serializer."
I think this is fine because even if the user doesn't realize that they can't return a string from it, it's not going to break anything, it might just make a little more work for them.
This looks good! I'm going to squash-and-merge it in now. The next steps are:
Thanks again for the PR! |
Out of curiosity @dbieber, do you have an idea of when you think this will make it to a public release? Not a huge deal, I'm just doing some planning |
I'd guess early September, but it's a high variance estimate. |
The release went out today. This is the most significant change in the release and is highlighted in the release notes. |
Amazing thanks for the release!! One thought that I've had since adding this functionality is that it'd be nice to be able to customize formatting based on the method called. For example class CLI:
def ls(self): pass
def get(self, id): pass
def serialize(result, calls):
if calls[-1].func == CLI.get: print("do something else")
... But that would break being able to do simple things like |
Fixes #344 (see issue for more details)
This lets you define a function that will take the result from the Fire component and allows the user to alter it before fire looks at it to render it.
Why? Because you want to define a global way of formatting your data for your CLI across a family of functions/classes where it is impractical (and a major pain) to wrap them all.
Outputting tabular data:
Outputs this:
But if we can define a formatter.
Outputs this:
Using a formatter means that:
lambda x: x
) then nothing is changede.g.
lambda x: {'idk why, but': x} if isinstance(x, list) else x
__str__
representation look like you want it to, you can handle it in the formatter insteade.g.
lambda x: custom_str(x) if isinstance(x, some_class) else x
yaml.dump
as a formatterlambda x: None