Skip to content

Instantly share code, notes, and snippets.

@predakanga
Last active November 22, 2018 10:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save predakanga/bb6d910ad9ba7ac7ef39d09d22ae3949 to your computer and use it in GitHub Desktop.
Save predakanga/bb6d910ad9ba7ac7ef39d09d22ae3949 to your computer and use it in GitHub Desktop.
# Because functions are variables, we can copy and delete the following function
def foo():
return True
bar = foo
del foo
# foo() would now cause an error
# bar() would return True
# This function is a decorator - when you use it, "f" will be the original function
def pointless(f):
print("I'm a pointless decorator! I don't change the function at all")
# Whatever you return will replace the original function
return f
# This...
@pointless
def foo():
return True
# Is equivalent to
foo = pointless(foo)
# Because decorators can replace the function, we can wrap or even replace the original function with our own logic
def negate(f):
def wrapper():
original_result = f()
return not original_result
return wrapper
@negate
def trueish():
return True
print(trueish())
# At this point, it's important to ask why we needed that weird nested function
# Why couldn't we just write...
def negate(f):
return not f()
# The reason is that the decorator has to return a *function*, which other code will eventually use
# It may help to imagine the whole .py file as one long script that gets run line-by-line when you call "import your_module"
from collections import defaultdict
# In this example, we'll assume a static rate limit of one call per 10s
# Adding arguments to decorators is a bit more complicated
# This means that you can make your own generic wrapper functions like the following:
def rate_limit(f):
# The wrapper function should have the same parameters as the original function
def wrapper(bot, trigger):
# We'll store our rate limit info in bot.memory
if not 'rate_limit_times' in bot.memory:
bot.memory['rate_limit_times'] = defaultdict(lambda: defaultdict(lambda: 0))
if not 'rate_limit_warned' in bot.memory:
bot.memory['rate_limit_warned'] = defaultdict(lambda: defaultdict(lambda: False))
# Again, cool Python things - the function can actually be a key for our dict
last_trigger = bot.memory['rate_limit_times'][f][trigger.nick]
if trigger.time < (last_trigger + 10):
# Rate limited - check if we should warn the user
if !bot.memory['rate_limit_warned'][f][trigger.nick]:
bot.reply("You can only use that command once every 10 seconds")
bot.memory['rate_limit_warned'][f][trigger.nick] = True
# Return, because we were rate limited
return
# Otherwise, call the original function
retval = f(bot, trigger)
# Account for NOLIMIT
if retval != NOLIMIT:
bot.memory['rate_limit_times'][f][trigger.nick] = trigger.time
bot.memory['rate_limit_warned'][f][trigger.nick] = False
# Finally, pass on the original function's return value
return retval
return wrapper
# Then you can use it like so
import random
@commands("test")
@rate_limit
def test_func(bot, trigger):
# Random reply to avoid the "..." issue
bot.reply(random.randint(1, 10))
# Finally, one important caveat
# With this simple example, @rate_limit has to be placed after any Sopel decorators, because of the way they work
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment