1
\$\begingroup\$

I'm trying to build a CLI that runs until the user decides to exit. The functionality of the CLI is to be able to explore a json-file by typing commands into the terminal while the program is running.

Right now commands are checked against if/else statements, but I would like to add so that commands are checked in the manner of the argparse module, but can't figure out how that is done while the program is running. Because I'm unable to figure that out, class methods are mapped to specific commands and executed when the command is specified without having to rely in if/else logic.

The biggest issue is not single commands as -v because they are easy to map to a function with dict. But commands that are -v LEAGUE where LEAGUE or -d LEAGUE SEASON TYPE is much harder to map without creating big dictionaries since the arguments can be vary. Any suggestion how that can be improved is highly appreciated!

import os
import sys


class CLIStats:

    def __init__(self):
        """Displays the welcome screen and loads the season-params.json
        which contains the avalible leagues and their seasons that
        can be downloaded. Pickled with pre-proccessed IDs is also loaded.
        """
        self.header = self.header()
        self.description = self.description()
        self.commands = self.commands()
        self.leagues = {'EN_PR': ['2019/2020', '2018/2019', '2017/2018', '2016/2017'],
                        'EU_CL': ['2019/2020', '2018/2019', '2017/2018', '2016/2017'],
                        'ES_PL': ['2019/2020', '2018/2019', '2017/2018', '2016/2017']}

    @staticmethod
    def header():
        """Display's welcome message"""
        os.system('clear')
        print('\t','*'*60)
        print("\t\t***  Welcome - Football Stats generator  ***")
        print('\t','*'*60)

    @staticmethod
    def description():
        """Display's short description"""
        print('Interface to download: \t playerstats \t fixturestats \t team standing \n')
        print('Type "exit" to terminate shell')

    @staticmethod
    def commands():
        """Display's avalible commands and how to use them"""
        commands = {'View Leagues': '-v',
                    'View League Seasons': '-v LEAGUE',
                    'Download stats': '-d LEAGUE SEASON TYPE',
                    'Help' : '-h',}
        for key, value in commands.items():
            print("{: <25} {}".format(key, value))
        print('\n')

    def view_leagues(self):
        """Prints out leagues in self.leagues"""
        for league in self.leagues.keys():
            print("{: <10}".format(league), end="")
        print('\n')

    def view_seasons(self, league):
        """Prints seasons for a league in self.leagues"""
        if league in self.leagues:
            seasons = self.leagues[league]
            print(league,'seasons:')
            for season in seasons:
                print("{: <20}".format(season), end="")
            print('\n')
        else:
            print(league, 'is not avalible')
            print('\n')

    def view_league_args(self):
        """Creates a list with set of args that can be passed
        by user to execute view_league()"""
        args = []
        for league in self.leagues.keys():
            args.append('-v' + ' ' +  league)
        return args  

def main():
    """Runs the interface"""
    interface = CLIStats()
    cmd = {'-v': interface.view_leagues,
           'exit': sys.exit,
           'View Stats Type': '-s',
           '-h' : 'interface.help', }
    while True:
        usr_command = input('CLIStats$ ')
        if usr_command in cmd.keys():
            cmd.get(usr_command)()

        elif usr_command in interface.view_league_args():
            league = usr_command.split(' ')[-1]
            interface.view_seasons(league)

        elif len(usr_command.split(' ')) == 4:
            league = usr_command.split(' ')[1]
            season = usr_command.split(' ')[2]
            stat_type = usr_command.split(' ')[3]
            interface.download_stats(league, season, stat_type)

        else:
            if usr_command.split(' ')[0] not in cmd.keys():
                print('Command not valid')
            else:
                print('Could not find the specified params')

if __name__ == '__main__':
    main()
\$\endgroup\$
1
  • \$\begingroup\$ Take a look at docopt. It is really a way to deal with arguments. \$\endgroup\$ – vnp Apr 23 '20 at 22:08
1
\$\begingroup\$

cmd.Cmd

Although argparse() could be abused to provide some of the desired functionality, it isn't the right tool. For example, it is designed to exit when an error is detected, such as an unrecognized command or missing argument.

cmd in the standard library is a much better tool for this. Writing a command interpreter is as simple as coding a subclass of cmd.Cmd and defining "do_command" methods for each command you need, e.g. do_view(), do_download(), etc. Here's an example based on your code, but the "do_command()" methods just print something.

import cmd 

class StatShell(cmd.Cmd):
    intro = '\n'.join([
        "\t" + "*"*60,
        "\t\t***  Welcome - Football Stats generator  ***",
        "\t" + "*"*60,
        "",
        "\tType help or ? to list commands,",
        "\t\tor help command to get help about a command."
    ])
    prompt = "CLIStats: "


    def do_download(self, arg):
        """Download statistics: download LEAGUE SEASON TYPE"""
        arg = arg.split()
        if len(arg) == 3:
            league, season, type = arg
            print(f"downloading {league} {season} {type}")
        else:
            self.error("bad 'download' command.")


    def do_exit(self, arg):
        """Exit the interpreter."""
        print("exiting ...")
        return True


    def do_view(self, arg):
        """view leagues or seasons: view [LEAGUE] """
        if arg:
            league = arg[0]
            print(league,'seasons: ...')

        else:
            print('leagues ...')


def main():
    StatShell().cmdloop()
\$\endgroup\$
1
  • \$\begingroup\$ thank you for providing an example, found out about cmd while digging into docopt that was mentioned by @vnp. I will certainly use this approach as it is exactly what I'm looking fot. Thank you for taking your time! \$\endgroup\$ – MisterButter Apr 24 '20 at 7:34
1
\$\begingroup\$

League representation

This:

    self.leagues = {'EN_PR': ['2019/2020', '2018/2019', '2017/2018', '2016/2017'],
                    'EU_CL': ['2019/2020', '2018/2019', '2017/2018', '2016/2017'],
                    'ES_PL': ['2019/2020', '2018/2019', '2017/2018', '2016/2017']}

seems excessive. At most, you should need to store:

  • The minimum and maximum years as int
  • The set of league names: {'EN_PR', 'EU_CL', 'ES_PL'}

Everything else can be derived.

Grammar

Display's avalible commands and how to use them

to

Displays available commands and how to use them

and

is not avalible

to

is not available

Generator

    args = []
    for league in self.leagues.keys():
        args.append('-v' + ' ' +  league)
    return args  

can be

return ' '.join(f'-v {league}' for league in self.league_names)

assuming that league_names holds the set previously described.

\$\endgroup\$
1
  • \$\begingroup\$ That you for your comments, self.leagues is normally a json file that is loaded so what you see is just a representation of it's content that is retrieved from an API. Thank you for taking your time to lift these things into light! \$\endgroup\$ – MisterButter Apr 24 '20 at 7:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.