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

Add decorators for callable attributes. #272

Merged
merged 7 commits into from
May 29, 2013
41 changes: 33 additions & 8 deletions willie/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,20 @@ def sub(pattern, self=self):
func.rate = 0

if hasattr(func, 'rule'):
if isinstance(func.rule, str):
pattern = sub(func.rule)
regexp = re.compile(pattern, re.I)
bind(self, func.priority, regexp, func)
rules = func.rule
if isinstance(rules, basestring):
rules = [func.rule]

if isinstance(rules, list):
for rule in rules:
pattern = sub(rule)
flags = re.IGNORECASE
if rule.find("\n") != -1:
flags |= re.VERBOSE
regexp = re.compile(pattern, flags)
bind(self, func.priority, regexp, func)

if isinstance(func.rule, tuple):
elif isinstance(func.rule, tuple):
# 1) e.g. ('$nick', '(.*)')
if len(func.rule) == 2 and isinstance(func.rule[0], str):
prefix, pattern = func.rule
Expand Down Expand Up @@ -238,9 +246,26 @@ def sub(pattern, self=self):

if hasattr(func, 'commands'):
for command in func.commands:
template = r'^%s(%s)(?: +(.*))?$'
pattern = template % (self.config.prefix, command)
regexp = re.compile(pattern, re.I)
# This regexp match equivalently and produce the same
# groups 1 and 2 as the old regexp: r'^%s(%s)(?: +(.*))?$'
# The only differences should be handling all whitespace
# like spaces and the addition of groups 3-6.
pattern = r"""
{prefix}({command}) # Command as group 1.
(?:\s+ # Whitespace to end command.
( # Rest of the line as group 2.
(?:(\S+))? # Parameters 1-4 as groups 3-6.
(?:\s+(\S+))?
(?:\s+(\S+))?
(?:\s+(\S+))?
.* # Accept anything after the parameters.
# Leave it up to the module to parse
# the line.
))? # Group 2 must be None, if there are no
# parameters.
$ # EoL, so there are no partial matches.
""".format(prefix=self.config.prefix, command=command)
regexp = re.compile(pattern, re.IGNORECASE | re.VERBOSE)
bind(self, func.priority, regexp, func)

class WillieWrapper(object):
Expand Down
211 changes: 211 additions & 0 deletions willie/module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
"""This module is meant to be imported from willie modules.

It defines the following decorators for defining willie callables:
willie.module.rule
willie.module.thread
willie.module.name (deprecated)
willie.module.command
willie.module.nickname_command
willie.module.priority
willie.module.event
willie.module.rate
willie.module.example
"""
"""
willie/module.py - Willie IRC Bot (http://willie.dftba.net/)
Copyright 2013, Ari Koivula, <[email protected]>

Licensed under the Eiffel Forum License 2.
"""


def rule(value):
"""Decorator. Equivalent to func.rule.append(value).

This decorator can be used multiple times to add more rules.

Args:
value: A regular expression which will trigger the function.

If the Willie instance is in a channel, or sent a PRIVMSG, where a string
matching this expression is said, the function will execute. Note that
captured groups here will be retrievable through the Trigger object later.

Inside the regular expression, some special directives can be used. $nick
will be replaced with the nick of the bot and , or :, and $nickname will be
replaced with the nick of the bot.

Prior to 3.1, rules could also be made one of three formats of tuple. The
values would be joined together to form a singular regular expression.
However, these kinds of rules add no functionality over simple regular
expressions, and are considered deprecated in 3.1.
"""
def add_attribute(function):
if not hasattr(function, "rule"):
function.rule = []
function.rule.append(value)
return function

if isinstance(value, tuple):
raise DeprecationWarning("Tuple-form .rule is deprecated in 3.1."
" Replace tuple-form .rule with a regexp.")

return add_attribute


def thread(value):
"""Decorator. Equivalent to func.thread = value.

Args:
value: Either True or False. If True the function is called in
a separate thread. If False from the main thread.
"""
def add_attribute(function):
function.thread = value
return function
return add_attribute


def name(value):
"""Decorator. Equivalent to func.name = value.

This attribute is considered deprecated in 3.1.
"""
raise DeprecationWarning("This attribute is considered deprecated in 3.1."
" Replace tuple-form .rule with a regexp.")


def command(value):
"""Decorator. Triggers on lines starting with "<command prefix>command".

This decorator can be used multiple times to add multiple rules. The
resulting match object will have the command as the first group, rest of
the line, excluding leading whitespace, as the second group. Parameters
1 through 4, seperated by whitespace, will be groups 3-6.

Args:
command: A string, which can be a regular expression.

Returns:
A function with a new regular expression appended to the rule
attribute. If there is no rule attribute, it is added.

Example:
@command("hello"):
If the command prefix is "\.", this would trigger on lines starting
with ".hello".
"""
def add_attribute(function):
if not hasattr(function, "commands"):
function.commands = []
function.commands.append(value)
return function
return add_attribute


def nickname_command(command):
"""Decorator. Triggers on lines starting with "$nickname: command".

This decorator can be used multiple times to add multiple rules. The
resulting match object will have the command as the first group, rest of
the line, excluding leading whitespace, as the second group. Parameters
1 through 4, seperated by whitespace, will be groups 3-6.

Args:
command: A string, which can be a regular expression.

Returns:
A function with a new regular expression appended to the rule
attribute. If there is no rule attribute, it is added.

Example:
@nickname_command("hello!"):
Would trigger on "$nickname: hello!", "$nickname, hello!",
"$nickname hello!", "$nickname hello! parameter1" and
"$nickname hello! p1 p2 p3 p4 p5 p6 p7 p8 p9".
@nickname_command(".*"):
Would trigger on anything starting with "$nickname[:,]? ", and
would have never have any additional parameters, as the command
would match the rest of the line.
"""
def add_attribute(function):
if not hasattr(function, "rule"):
function.rule = []
rule = r"""
^
$nickname[:,]? # Nickname.
\s+({command}) # Command as group 0.
(?:\s+ # Whitespace to end command.
( # Rest of the line as group 1.
(?:(\S+))? # Parameters 1-4 as groups 2-5.
(?:\s+(\S+))?
(?:\s+(\S+))?
(?:\s+(\S+))?
.* # Accept anything after the parameters. Leave it up to
# the module to parse the line.
))? # Group 1 must be None, if there are no parameters.
$ # EoL, so there are no partial matches.
""".format(command=command)
function.rule.append(rule)
return function

return add_attribute


def priority(value):
"""Decorator. Equivalent to func.priority = value.

Args:
value: Priority can be one of "high", "medium", "low". Defaults to
medium.

Priority allows you to control the order of callable execution, if your
module needs it.
"""
def add_attribute(function):
function.priority = value
return function
return add_attribute


def event(value):
"""Decorator. Equivalent to func.event = value.

This is one of a number of events, such as 'JOIN', 'PART', 'QUIT', etc.
(More details can be found in RFC 1459.) When the Willie bot is sent one of
these events, the function will execute. Note that functions with an event
must also be given a rule to match (though it may be '.*', which will
always match) or they will not be triggered.
"""
def add_attribute(function):
function.event = value
return function
return add_attribute


def rate(value):
"""Decorator. Equivalent to func.rate = value.

Availability: 2+

This limits the frequency with which a single user may use the function. If
a function is given a rate of 20, a single user may only use that function
once every 20 seconds. This limit applies to each user individually. Users
on the admin list in Willie’s configuration are exempted from rate limits.
"""
def add_attribute(function):
function.rate = value
return function
return add_attribute


def example(value):
"""Decorator. Equivalent to func.example = value.

This doesn't do anything yet.
"""
def add_attribute(function):
function.example = value
return function
return add_attribute
15 changes: 11 additions & 4 deletions willie/modules/dice.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@

from random import randint, seed, choice
from willie.modules.calc import calculate
import willie.module
import re

seed()


@willie.module.command("roll")
@willie.module.command("dice")
@willie.module.command("d")
@willie.module.priority("medium")
def dice(willie, trigger):
"""
.dice <formula> - Rolls dice using the XdY format, also does basic math and
Expand Down Expand Up @@ -74,8 +79,6 @@ def dice(willie, trigger):
else:
willie.reply('You roll ' + trigger.group(2) + ': ' + full_string + ' = '
+ result)
dice.commands = ['roll', 'dice', 'd']
dice.priority = 'medium'


def rollDice(diceroll):
Expand All @@ -91,6 +94,11 @@ def rollDice(diceroll):
randint(1, size))[randint(0, 9)])
return sorted(result) # returns a set of integers.


@willie.module.command("choice")
@willie.module.command("ch")
@willie.module.command("choose")
@willie.module.priority("medium")
def choose(willie, trigger):
"""
.choice option1|option2|option3 - Makes a difficult choice easy.
Expand All @@ -100,8 +108,7 @@ def choose(willie, trigger):
choices = re.split('[\|\\\\\/]', trigger.group(2))
pick = choice(choices)
return willie.reply('Your options: %s. My choice: %s' % (', '.join(choices), pick))
choose.commands = ['choice', 'ch', 'choose']
choose.priority = 'medium'


if __name__ == '__main__':
print __doc__.strip()
Expand Down
18 changes: 9 additions & 9 deletions willie/modules/reload.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
import os.path
import time
import imp
import willie.irc
import willie.module
import subprocess


@willie.module.nickname_command("reload")
@willie.module.priority("low")
@willie.module.thread(False)
def f_reload(willie, trigger):
"""Reloads a module, for use by admins only."""
if not trigger.admin:
Expand Down Expand Up @@ -51,10 +54,7 @@ def f_reload(willie, trigger):
willie.bind_commands()

willie.reply('%r (version: %s)' % (module, modified))
f_reload.name = 'reload'
f_reload.rule = ('$nick', ['reload'], r'(.+)?')
f_reload.priority = 'low'
f_reload.thread = False


if sys.version_info >= (2, 7):
def update(willie, trigger):
Expand All @@ -74,6 +74,9 @@ def update(willie, trigger):
update.rule = ('$nick', ['update'], r'(.+)')


@willie.module.nickname_command("load")
@willie.module.priority("low")
@willie.module.thread(False)
def f_load(willie, trigger):
"""Loads a module, for use by admins only."""
if not trigger.admin:
Expand Down Expand Up @@ -107,10 +110,7 @@ def f_load(willie, trigger):
willie.bind_commands()

willie.reply('%r (version: %s)' % (module, modified))
f_load.name = 'load'
f_load.rule = ('$nick', ['load'], r'(.+)?')
f_load.priority = 'low'
f_load.thread = False


if __name__ == '__main__':
print __doc__.strip()