I tried to create a simple decorator to use with the Slack API to make further coding, to improve functionality later much easier and more organized (say, in the future, I wished to include a new function/command that I wanted the bot to do)
But it's not working as I expected. Let me explain, here's my code (I have left out large chunks of code, such as instantiation, token, etc. to keep the question brief and illustrate the salient points) It includes a simple function which is supposed to output a message in the Slack channel:
class Bot(object):
def __init__(self):
# Bot's user ID in Slack: value is assigned after the bot starts up
self.starterbot_id = None
# Declare constants
self.RTM_READ_DELAY = 1 # 1 second delay between reading from RTM
self.EXAMPLE_COMMAND = "do"
self.MENTION_REGEX = "^<@(|[WU].+?)>(.*)"
def startup(self):
#Startup the client
if slack_client.rtm_connect(with_team_state=False):
print("Slack Bot connected and running!")
# Read bot's user ID by calling Web API method `auth.test`
self.starterbot_id = slack_client.api_call("auth.test")["user_id"]
while True:
command, channel = self.parse_bot_commands(slack_client.rtm_read())
if command:
response = self.command(command)
self.handle_command(response, channel)
time.sleep(self.RTM_READ_DELAY)
else: print("Connection failed. Exception traceback printed above.")
def handle_command(self, response, channel):
"""
Executes bot command if the command is known
"""
# Default response is help text for the user
default_response = "Not sure what you mean. Try *{}*.".format(self.EXAMPLE_COMMAND)
# Sends the response back to the channel
slack_client.api_call("chat.postMessage", channel=channel, text=response or default_response)
def command(self, func):
"""
Simple wrapper for bot commands
"""
def wrapper_command():
func()
return wrapper_command
'''USAGE:'''
bot = Bot()
bot.startup()
@bot.command
def greet():
response = "Hi there I am CodeMonkey"
return response
What I was looking for was for the greet()
command to get the bot to output “Hi there I am CodeMonkey”
The bot did respond, but not with a greeting. Instead it returns the object. This is the response I get from the bot in the channel:
CodeMonkey APP [11:40 PM]
<function Bot.command.<locals>.wrapper_command at 0x1083281e0>
I am still learning to use decorators and I feel that they will make coding my Slack Bot much easier and more organized, but what am I doing wrong?
Well, the response you get is exactly what one would expect from this:
response = self.command(command)
self.handle_command(response, channel)
and this:
def command(self, func):
"""
Simple wrapper for bot commands
"""
def wrapper_command():
func()
return wrapper_command
You call self.command(...)
and pass the result to self.handle_command()
, self.command()
returns a function, so self.handle_command()
get this question as the response
parameter.
You still didn't explain what you expected command()
to do when used as a decorator, but I assume what you want is to use the decorated function as handler for a given 'command' (the parameter, not the function - hint: use distinct names for distinct things), ie self.command('greet')
should call the greet
function.
The very first thing you want is to use distinct methods for executing commands and registering them - so we'll name those methods execute_command
and register_command
- and while we're at it, we'll rename "handle_command" to "send_response" because that's what it's really doing:
def startup(self):
#Startup the client
if slack_client.rtm_connect(with_team_state=False):
print("Slack Bot connected and running!")
# Read bot's user ID by calling Web API method `auth.test`
self.starterbot_id = slack_client.api_call("auth.test")["user_id"]
while True:
command, channel = self.parse_bot_commands(slack_client.rtm_read())
if command:
response = self.execute_command(command)
self.send_response(response, channel)
time.sleep(self.RTM_READ_DELAY)
else:
print("Connection failed. Exception traceback printed above.")
def execute_command(self, command):
"""
Executes bot command if the command is known
"""
# mock it for the moment
return "this is a mockup"
def send_response(self, response, channel):
# Default response is help text for the user
default_response = "Not sure what you mean. Try *{}*.".format(self.EXAMPLE_COMMAND)
# Sends the response back to the channel
slack_client.api_call("chat.postMessage", channel=channel, text=response or default_response)
def register_command(self, func):
"""
TODO
"""
Now for the decorator part. Wrapping functions into other functions that will execute "around" the decorated one is a common use case for decorators, but that doesn't mean it's mandatory. technically, a "decorator" is just an "higher order function": a function that takes a function as argument and returns a function as result. Actually, the @decorator
syntax is only syntactic sugar, so
@decorate
def func():
pass
is just a fancier way to write
def func():
pass
func = decorate(func)
and once you understand this you also understand that there's absolutely nothing magical here.
Now back to our register_command
decorator. What we want is to store the argument (a function) so we can retrieve it based on it's name. This is easily done with a dict and func.__name__
:
def __init__(self, ...):
# init code here
self._commands = {}
def register_command(self, func):
self._commands[func.__name__] = func
# and that's all - no wrapper needed here
return func
So you can now use
@bot.register_command:
def greet():
return "Hello"
and then bot._commands
should contain 'greet': <function greet @...>
.
Now for the execute_command
method - well, it's quite straigtforward: lookup self._commands, if something is found call it and return the response, else return the default response:
def execute_command(self, command):
handler = self._commands.get(command)
if handler:
return handler()
# default:
return "Not sure what you mean. Try *{}*.".format(self.EXAMPLE_COMMAND)
and we can now simplify send_response()
since execute_command
will take care of providing the default response:
def send_response(self, response, channel):
# Sends the response back to the channel
slack_client.api_call("chat.postMessage", channel=channel, text=response)