Skip to content
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

Get a list of all valid commands when writing --help #225

Open
bfelbo opened this issue Feb 19, 2020 · 18 comments · May be fixed by #364
Open

Get a list of all valid commands when writing --help #225

bfelbo opened this issue Feb 19, 2020 · 18 comments · May be fixed by #364
Labels

Comments

@bfelbo
Copy link

bfelbo commented Feb 19, 2020

Thanks for creating this amazing library. I'm somewhat confused as to how the --help command is implemented. When running python example.py --help Fire only prints the flags, not the actual commands that can be run (see example below):

NAME
    fury.py

SYNOPSIS
    fury.py <flags>

FLAGS
    --verbosity=VERBOSITY

How do I make --help show all the possible commands and why is that not the default? :)

@dbieber
Copy link
Member

dbieber commented Feb 21, 2020

Hi @bfelbo,

Are you suggesting that the help show:

fury.py --verbosity=VERBOSITY

or is there a command that's actually missing from the help, that you'd like it to show?

It should be showing all the possible commands, but not listed out as all possible complete commands explicitly simply due to formatting considerations. In particular, all commands share a prefix, and sometimes that prefix is quite long, so we choose not to write out that prefix repeatedly. Always open to suggestions for improvement though.

@bfelbo
Copy link
Author

bfelbo commented Feb 21, 2020

Thanks for the response. I'll make it more clear :) See the below example from the docs.

import fire

class Calculator(object):

  def add(self, x, y):
    return x + y

  def multiply(self, x, y):
    return x * y

if __name__ == '__main__':
  fire.Fire(Calculator)

Right now running python example.py --help returns the following:

NAME
    example.py

SYNOPSIS
    example.py

I think it'd be great if running help would actually tell you what CLI commands are available, i.e. add and multiply in this case. Otherwise, how can you figure out what you can do with the CLI? Maybe I'm missing something obvious here :)

@bfelbo
Copy link
Author

bfelbo commented Feb 21, 2020

It should be showing all the possible commands

This doesn't seem to be the case? (see my previous comment and example)

@saltyfireball
Copy link

@bfelbo As far as I know, it doesn't generate the options for you. You're saying it would be nice if help also showed something like this?

add - examply.py add x y
multiply - examply.py multiply x y

I don't think that would be too hard to add-in.

@bfelbo
Copy link
Author

bfelbo commented Feb 24, 2020

Exactly. That would be amazing!

For the class-based interface, I would probably ignore the self argument. It would then become something like the following:

NAME
    example.py

COMMANDS
    add x y
    multiply x y

SYNOPSIS
    example.py

@bfelbo
Copy link
Author

bfelbo commented Feb 24, 2020

If the arguments have a default, it'd be great to include them too, e.g.:

NAME
    example.py

COMMANDS
    add x y
    multiply x y
    root x z=2

SYNOPSIS
    example.py

I'm not really sure what the SYNOPSIS part is useful for, even in the case of NAME != SYNOPSIS. If you're worried the output gets too long, it would perhaps make sense to remove it?

@saltyfireball
Copy link

@bfelbo so I was just playing around. If you return self from your functions you can get a little bit closer to what you're looking for:

import fire
class Calculator(object):
    """A simple calculator class."""
    def __init__(self, meow=True):
        self.meow=meow
        self.db = None

    def double(self, number):
        self.db = 2 * number
        return self

    def add(self, number):
        self.db = 2 * number
        return self

if __name__ == '__main__':
    fire.Fire(Calculator)

Now when I run this I get a help screen with COMMANDS:

python runner.py double 10
NAME
    runner.py double 10 - A simple calculator class.

SYNOPSIS
    runner.py double 10 COMMAND | VALUE

DESCRIPTION
    A simple calculator class.

COMMANDS
    COMMAND is one of the following:

     add

     double

@saltyfireball
Copy link

saltyfireball commented Feb 24, 2020

@bfelbo Ignoring that my example isn't really functional, it does show that COMMANDS are parsed at some point. and we could make use of that to extend the general response from --help

This would be a nice upgrade - and I think fits the theme of bootstrapping classes into a more friendly cli.

@dbieber
Copy link
Member

dbieber commented Feb 24, 2020

Agreed this is an issue. Let me explain why the current behavior is as it is, and how we can probably fix it.

Current State

"--help" vs "-- --help"

--help, --interactive, --verbose, etc are special Fire arguments. Normally they are separated from the standard arguments to a Fire CLI by a separating --. This is to avoid confusion between arguments intended for the CLI developers functions and arguments intended for Fire itself.

However, in order to let users get help the way they expect, we make an exception for --help. (Eventually we may want to broaden this exception to all of Fire's args, but for now it's just for --help.) This means that if a user specifies "--help" or "-h" and the CLI isn't expecting an argument named "help" or "h", we'll show the user help for the current component.

Help for Objects vs Help for Classes

The help screen is meant to show all the commands for the current component. If the current component is a class, often the only thing you can do is initialize it. Only after it's initialized does it make sense to access and call its methods. If the current component is an object (which happens for example after a class has been initialized) then the methods on that object are shown in the help as commands.

You can use a separator (-) to indicate that you're done passing arguments to a component. Using a separator will cause a class to be initialized without waiting for additional arguments.

Getting Help Today

class --help # Shows help for the class. This is often unhelpful, and is the main issue in this issue.
class -- --help # Same as previous
class - --help # Instantiates an object of class class, then shows help for that object. This is more useful help, but unintuitive to users.
class - -- --help # Same as previous.

Next Steps [Edit: never mind, see next comment instead]

The solution will be to make class --help equivalent to class - -- --help, rather than to class -- --help as it is today.

References in the Code

Here is where Fire checks to see if the user is requesting help without a separating --.

https://github.com/google/python-fire/blob/1ac5105a0437518785033f47dd78746678e5133a/fire/core.py#L438-439

The logic for separators is at

python-fire/fire/core.py

Lines 442 to 450 in 1ac5105

saved_args = []
used_separator = False
if separator in remaining_args:
# For the current component, only use arguments up to the separator.
separator_index = remaining_args.index(separator)
saved_args = remaining_args[separator_index + 1:]
remaining_args = remaining_args[:separator_index]
used_separator = True
assert separator not in remaining_args

and

if used_separator:

Basically what we'll have to change is:

Instead of immediately breaking at 439 when --help is observed, we'll first do one more pass through the big while loop as if there was a separator, and then we'll break to show help.

@dbieber
Copy link
Member

dbieber commented Feb 24, 2020

Actually, perhaps it's even better to just show the methods of a class in that class's help screen, even if the class hasn't been instantiated yet. We'll just need to be careful in the presentation to make clear which flags are required (because the methods can't be called until after the class is initialized).

@saltyfireball
Copy link

saltyfireball commented Feb 24, 2020

Actually, perhaps it's even better to just show the methods of a class in that class's help screen, even if the class hasn't been instantiated yet. We'll just need to be careful in the presentation to make clear which flags are required (because the methods can't be called until after the class is initialized).

@dbieber I think this would work perfectly. Like you previously outlined

class --help 

Should just go into details on that class, like an improved version of what class - --help does.

@dbieber
Copy link
Member

dbieber commented Feb 24, 2020

The help text is generated at:

def HelpText(component, trace=None, verbose=False):

The list of possible actions (including commands) is generated by

actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose)

The commands are aggregated at:

if value_types.IsCommand(member):
commands.Add(name=member_name, member=member)

And isCommand is defined at:

def IsCommand(component):
return inspect.isroutine(component) or inspect.isclass(component)


The actual string formatting to build the help text starts at

# Sections:

@dbieber
Copy link
Member

dbieber commented Feb 24, 2020

Here's a minimal example that would need to be fixed to resolve this:

In [1]: class A: 
   ...:     def x(self): 
   ...:         return 0 
   ...:                                                                                                                                                           

In [5]: import fire                                                                                                                                               

In [6]: fire.completion.VisibleMembers(A)                                                                                                                         
Out[6]: []  # This will need to include x.

In [7]: fire.completion.VisibleMembers(A())                                                                                                                       
Out[7]: [('x', <bound method A.x of <__main__.A object at 0x10af2c790>>)]

VisibleMembers is defined here:

def VisibleMembers(component, class_attrs=None, verbose=False):

@dbieber
Copy link
Member

dbieber commented Feb 24, 2020

The code ignoring methods in classes is here:

if class_attr and class_attr.kind in ('method', 'property'):
# methods and properties should be accessed on instantiated objects,
# not uninstantiated classes.
return False

Simply changing that from False to True will result in methods being included in the help. Additional changes will likely also be needed to make sure flags are marked as required when appropriate so we don't falsely give the impression that these methods are callable before the class has been instantiated.

@tbenst
Copy link

tbenst commented Apr 11, 2020

Thanks for this!

falsely give the impression that these methods are callable before the class has been instantiated.

Perhaps wise to commit the current solution and refine per this comment later? Status quo help message is a bit tricky unless user is familiar with fire, and the solution you sketched out is a big improvement!

@dbieber dbieber added the bug label Apr 12, 2020
@impredicative
Copy link

impredicative commented Aug 20, 2020

This is desired basic functionality for any CLI argument parser. Is there an alternative straightforward tool that does this?

@jwickens
Copy link

Would be happy to help and submit a PR along the lines of @dbieber's last comment if a maintainer is open to it.

@kylemcdonald
Copy link

Just reviving this old thread to say: showing the methods of a class was my expected default behavior for "--help" and I was very surprised when I discovered it did not work as expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants