typed-argparse / typed-argparse

💡 write type-safe and elegant CLIs with a clear separation of concerns.
https://typed-argparse.github.io/typed-argparse/
MIT License
27 stars 6 forks source link

Feature request: high-level APIs #32

Open pdc1 opened 1 year ago

pdc1 commented 1 year ago

Hi there,

I am reworking my low-level API app, and I'm liking it a lot, it really helps clean up the app logic and flow.

In the process, I have found a few things that would be helpful:

  1. The ability to provide a help string for each subparser. The low-level API shows a list of subparsers, each with their own help. The high-level just lists them in a list as positional arguments.

  2. The ability to provide a description string for the parser itself, equivalent to argparse.ArgumentParser(description="description").

  3. The ability to define a function to be run before running the bindings. For example, one of my common args is a logging level. I need a function to set the logging level prior to running the bound function. My workaround is to call a common pre-process method in each "run" method. I could imagine a need for a post-process method as well, to free resources, though I don't need that currently.

bluenote10 commented 1 year ago

The ability to provide a help string for each subparser. The low-level API shows a list of subparsers, each with their own help. The high-level just lists them in a list as positional arguments.

Yes, that is definitely planned. I hope I can add it in the next days.

The ability to provide a description string for the parser itself, equivalent to argparse.ArgumentParser(description="description").

That should already be possible, right? The high level Parser already forwards various native argparse arguments:

https://github.com/typed-argparse/typed-argparse/blob/998309e253282113f2125207f8d6f4d3b4fd3c24/typed_argparse/parser.py#L107

But I should add an example in the documentation.

The ability to define a function to be run before running the bindings. For example, one of my common args is a logging level. I need a function to set the logging level prior to running the bound function. My workaround is to call a common pre-process method in each "run" method. I could imagine a need for a post-process method as well, to free resources, though I don't need that currently.

I would have to think about it some more, but why does the pre- and post-processing have to go into the framework? Why not just something like:

def main():
    do_something_when_app_starts()
    Parser(...).bind(...).run()
    do_something_when_app_ends()

Note that as soon as these pre- and post-processing steps need access to arguments they obviously have to go into the "runner" function, i.e., the pattern would simply become:

def run(args: MyArgs):
    do_something_when_app_starts(args)
    # actual run logic...
    do_something_when_app_ends(args)

With multiple subparsers there is again the problem that they can be in theory completely different, but a similar pattern would be possible when they inherit from some CommonArgs.

pdc1 commented 1 year ago

The ability to provide a help string for each subparser.

Yes, that is definitely planned. I hope I can add it in the next days.

Nice, thank you!

The ability to provide a description string for the parser itself, equivalent to argparse.ArgumentParser(description="description").

That should already be possible, right? The high level Parser already forwards various native argparse arguments: [...]

But I should add an example in the documentation.

Oh, sorry about that. I looked through that file for the subparser options, but didn't think to do so for the parser. An example might help.

The ability to define a function to be run before running the bindings. [...]

I would have to think about it some more, but why does the pre- and post-processing have to go into the framework? [...] Note that as soon as these pre- and post-processing steps need access to arguments they obviously have to go into the "runner" function, i.e., the pattern would simply become:

def run(args: MyArgs):
    do_something_when_app_starts(args)
    # actual run logic...
    do_something_when_app_ends(args)

With multiple subparsers there is again the problem that they can be in theory completely different, but a similar pattern would be possible when they inherit from some CommonArgs.

I might be missing something. I have nine subparsers and a common definition for shared args, like logging, debug, verbose, etc. I am using the common_args=CommonArgs option to SubParserGroup, but I am not sure how to associate common functionality. I tried binding a common functionality function to CommonArgs, but it did not run, only the subparser function.

Since there is a common_args= directive to the args definition, I was hoping the framework might include a binding for those common args that would run prior to subprocessor bindings.

To help explain, my preprocessing is handling the CommonArgs to (a) do extra validation on the args, e.g. make sure combinations of arguments are valid, and (b) do some setup based on the args, e.g. set logging levels to the specified value (like --log debug).

Currently I have every subparser function start with a call to preprocessing(args: CommonArgs) (which might be what you were suggesting!) but that is error-prone if I forget one. Still, this approach is straightforward, and I can see working in some kind of automatic shared functionality might be tricky.

bluenote10 commented 1 year ago

I might be missing something. I have nine subparsers and a common definition for shared args, like logging, debug, verbose, etc. I am using the common_args=CommonArgs option to SubParserGroup, but I am not sure how to associate common functionality.

I guess now I understand your feature request correctly. On sub-parser level the idea sounds good. I thought you mean even on top-level (no sub-parsers involved). Since both the pre-processing and post-processing functions would correspond to the type specified in common_args it would perhaps make sense to name the arguments accordingly common_preprocessing and common_postprocessing.

Maybe the names "preprocessing" and "postprocessing" aren't ideal yet, because they are typically used for data transformation operations, and in this case the functions are rather side-effects. Alternatives: common_setup + common_teardown, common_pre_execution + common_post_execution, ...

Things could get tricky if the setup functions are supposed to return something that should get passed to the runner functions.

pdc1 commented 1 year ago

I like common_pre_execution and common_post_execution, they seem descriptive without implying what would or would not be part of the function.

As for passing things to the runner functions, in theory I suppose you could define fields in the common arg object that were not actually arguments, perhaps prefixed with '_', but I don't have a need for that so you could think about that as a future enhancement.

bluenote10 commented 1 year ago

The ability to provide a help string for each subparser.

Yes, that is definitely planned. I hope I can add it in the next days.

FYI this should be available now in version 0.2.8.

pdc1 commented 1 year ago

The ability to provide a help string for each subparser.

Yes, that is definitely planned. I hope I can add it in the next days.

FYI this should be available now in version 0.2.8.

Very nice! Thank you :)