5

I am creating a chatbot that has 2 main files, main.py which runs the bot and commands.py which contains the commands the bot recognizes. I am trying to get the function names from commands.py in order to use that information (as a simple string/store as a variable) in main.py. For example:

commands.py

def add():
    pass

def delete():
    pass

def change():
    pass

Basically, I want to be able to store in a variable commands = ['add', 'delete', 'change'] or something similar. I don't have any experience with decorators, but is this possibly a good time to use it? To be able to register commands with a decorator? I'm open to any suggestions, thanks!

Neemaximo
  • 20,031
  • 12
  • 32
  • 40

3 Answers3

4

IIUC,

import commands

>>> dir(commands)
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'add',
 'delete',
 'change']

Then filter out the magic attrs

>>> function_names = [func for func in dir(commands) if not func.startswith('__')]

['add', 'delete', 'change']
rafaelc
  • 57,686
  • 15
  • 58
  • 82
  • Wouldn't defining an [`__all__`](https://stackoverflow.com/a/64130/2588818) be a much more straightforward implementation of the same idea? – Two-Bit Alchemist Jul 12 '18 at 18:34
  • Why straightforward? What if you have 200 methods? You'd hardcode 200 strings in `__all__`? – rafaelc Jul 12 '18 at 18:36
  • Then I would use the class-based approach I suggested. Straightforward because it's the intended purpose of `__all__` while rifling through a module `dir()` is unusual. It's not clear what's intended from just the list comprehension which is all that will appear in OP's code. – Two-Bit Alchemist Jul 12 '18 at 18:38
  • The only downside in my solution is if you *also* have attributes in your file. Then, var names would show up along with function names. – rafaelc Jul 12 '18 at 18:40
  • 1
    Attributes, or if you import anything else, or helper functions. This is deceptively simple until you try maintaining a sizeable program (200 commands, as you say) and keep accidentally ending up with junk in your dynamic array of commands. – Two-Bit Alchemist Jul 12 '18 at 18:41
1

I did this very recently like so:

def is_command(class_method):
    class_method.is_command = True
    return classmethod(class_method)

Then group your commands in a class:

class CommandsModule:
    @is_command
    def add(cls):
        pass

The decorator isn't strictly necessary but neatly separates your actual commands from other helper functions that shouldn't be 'callable' by the interface. To get the commands I had a function (in another class, in my case) like so:

def get_command(self, command_word):
    if not command_word:
        return

    command_func = getattr(CommandsModule, str(command_word).lower(), None)
    if (not callable(command_func) or
            not getattr(command_func, 'is_command', False)):
        return

    return command_func

From here it is quite easy to add extra features, like better error checking around commands, or commands with arguments. Adding a new command is as easy as writing a CommandsModule method and putting your decorator to indicate it's callable.

Two-Bit Alchemist
  • 17,966
  • 6
  • 47
  • 82
  • Thanks for the reply, I'm interested to hear how you ended up using this if you don't mind. i.e. did you just use it to verify if a command is valid/callable? Or did you then use it to also call the command? thanks! – Neemaximo Jul 12 '18 at 19:23
  • I used it for both. I had a `call_command` function which takes a user-supplied command (from chat) and optionally any arguments following it. `call_command` calls `get_command` with the supplied name. If it is not valid, `get_command` returns `None`. Otherwise, you get the correct function, ready to call. The rest of `call_command` was for handling command arguments and error checking. – Two-Bit Alchemist Jul 13 '18 at 13:00
0

You do not have to use a decorator. Instead, use getattr:

import commands #the file with bot operations
val1 = getattr(commands, 'add')()
val2 = getattr(commands, 'delete')()
val3 = getattr(commands, 'change')()
Ajax1234
  • 69,937
  • 8
  • 61
  • 102
  • This makes no sense. He wnats to know *add*, *delete* and *change*. If he is to hardcode these values, then what's the point? – rafaelc Jul 12 '18 at 18:29
  • This doesn't group your commands together except that you have hard-coded all the command names. This works, but won't lead to a well-structured program. – Two-Bit Alchemist Jul 12 '18 at 18:29
  • @RafaelC As the OP has hinted, he is attempting to access objects from `commands.py` by string (`['add', 'delete', 'change']`). `getattr` will enable him to do that by passing the string value. The names are hardcoded for demonstration purposes. – Ajax1234 Jul 12 '18 at 18:33
  • @Ajax1234 Hmm, I see. To be honest I interpreted it in a different way, let's wait for OP to clarify – rafaelc Jul 12 '18 at 18:34
  • @RafaelC Actually, your answer makes more sense relative to the OP's question context. Perhaps the OP can clarify. – Ajax1234 Jul 12 '18 at 18:35
  • Hi all, at this point I was attempting to access the objects, but merely store the commands as a string so I can validate if a user's message to the chatbot is a known command (simply by comparing strings). Hope that makes sense and sorry for the confusion. – Neemaximo Jul 12 '18 at 18:40