Skip to content
This repository has been archived by the owner on Apr 19, 2019. It is now read-only.

Commit

Permalink
Add SubcommandEventPlugin convenience class
Browse files Browse the repository at this point in the history
  • Loading branch information
kxz committed Mar 26, 2016
1 parent e9a1e62 commit f6fe808
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 122 deletions.
10 changes: 10 additions & 0 deletions docs/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ prefix ending with a period (``.``).
Undotted names are reserved for Omnipresence core variables.


Command base classes
====================

Omnipresence provides classes for common types of command plugins.
As with the standard `EventPlugin` class, they are intended to be
subclassed, not instantiated.

.. autoclass:: SubcommandEventPlugin(bot)


Writing tests
=============

Expand Down
74 changes: 73 additions & 1 deletion omnipresence/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,78 @@ def respond_to(self, msg):
return maybeDeferred(callback, msg)


class SubcommandEventPlugin(EventPlugin):
"""A base class for command plugins that invoke subcommands given in
the first argument by invoking one of the following methods:
#. ``on_empty_subcommand(msg)``, if no arguments are present. The
default implementation raises a `UserVisibleError` asking the
user to provide a valid subcommand.
#. ``on_subcommand_KEYWORD(msg, remaining_args)``, if such a method
exists.
#. Otherwise, ``on_invalid_subcommand(msg, keyword,
remaining_args)``, which by default raises an "unrecognized
command" `UserVisibleError`.
``on_cmdhelp`` is similarly delegated to ``on_subcmdhelp`` methods:
#. ``on_empty_subcmdhelp(msg)``, if no arguments are present. The
default implementation lists all available subcommands.
#. ``on_subcmdhelp_KEYWORD(msg)``, if such a method exists.
#. Otherwise, ``on_invalid_subcmdhelp(msg, keyword)``, which by
default simply calls ``on_empty_subcmdhelp``.
As with ``on_cmdhelp``, the subcommand keyword is automatically
added to the help string, after the containing command's keyword and
before the rest of the string.
"""

@property
def _subcommands(self):
return sorted(name[14:] for name in dir(self)
if name.startswith('on_subcommand_'))

def on_command(self, msg):
args = msg.content.split(None, 1)
if args:
callback_name = 'on_subcommand_' + args[0]
subargs = '' if len(args) < 2 else args[1]
if hasattr(self, callback_name):
return getattr(self, callback_name)(msg, subargs)
return self.on_invalid_subcommand(msg, args[0], subargs)
return self.on_empty_subcommand(msg)

def on_cmdhelp(self, msg):
if not msg.content:
return self.on_empty_subcmdhelp(msg)
callback_name = 'on_subcmdhelp_' + msg.content
if hasattr(self, callback_name):
return '\x02{}\x02 {}'.format(
msg.content, getattr(self, callback_name)(msg))
return self.on_invalid_subcmdhelp(msg, msg.content)

def on_empty_subcommand(self, msg):
raise UserVisibleError(
'Please provide a subcommand: \x02{}\x02.'
.format('\x02, \x02'.join(self._subcommands)))

def on_empty_subcmdhelp(self, msg):
return '\x02{}\x02'.format('\x02|\x02'.join(self._subcommands))

def on_invalid_subcommand(self, msg, keyword, args):
raise UserVisibleError(
'Unrecognized subcommand \x02{}\x02. Valid subcommands: '
'\x02{}\x02.'.format(
keyword, '\x02, \x02'.join(self._subcommands)))

def on_invalid_subcmdhelp(self, msg, keyword):
return self.on_empty_subcmdhelp(msg)


def plugin_class_by_name(name):
"""Return an event plugin class given the *name* used to refer to
it in an Omnipresence configuration file."""
Expand All @@ -80,6 +152,6 @@ def plugin_class_by_name(name):
class UserVisibleError(Exception):
"""Raise this inside a command callback if you need to return an
error message to the user, regardless of whether or not the
``show_errors`` configuration option is enabled. Errors are always
``show_errors`` configuration option is enabled. Errors are always
given as replies to the invoking user, even if command redirection
is requested."""
229 changes: 112 additions & 117 deletions omnipresence/plugins/dice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from ...humanize import andify
from ...message import collapse
from ...plugin import EventPlugin, UserVisibleError
from ...plugin import SubcommandEventPlugin, UserVisibleError


#: The maximum number of dice that can be rolled at once.
Expand All @@ -25,7 +25,7 @@ def format_rolls(rolls):
' '.join(str(r) for r in sorted(rolls)), sum(rolls))


class Default(EventPlugin):
class Default(SubcommandEventPlugin):
def __init__(self):
#: User die banks, keyed by a (channel, nick) tuple.
self.banks = defaultdict(Counter)
Expand Down Expand Up @@ -64,65 +64,118 @@ def roll_dice(self, dice):
else self.random.randint(1, size))
return rolls

def on_command(self, msg):
def reply_for_roll(self, msg, args, update=False, clear=False):
nick = msg.connection._lower(msg.actor.nick)
args = (msg.content or 'show').split(None, 1)
subcommand = args[0]
if subcommand == 'show':
if len(args) < 2:
# Show the actor's own die bank if no nick is provided.
requested_nick = nick
else:
requested_nick = msg.connection._lower(args[1])
rolls = self.banks[(msg.venue, requested_nick)].elements()
return 'Bank has {}.'.format(format_rolls(rolls))
if subcommand in ('roll', 'add', 'new'):
if len(args) < 2:
raise UserVisibleError('Please specify dice to roll.')
if not args:
raise UserVisibleError('Please specify dice to roll.')
try:
rolls = self.roll_dice(args.split())
except ValueError as e:
raise UserVisibleError(str(e))
message = 'Rolled {}.'.format(format_rolls(rolls))
if update:
if clear:
self.banks.pop((msg.venue, nick), None)
bank = self.banks[(msg.venue, nick)]
bank.update(rolls)
message += ' Bank now has {}.'.format(
format_rolls(bank.elements()))
return message

def on_empty_subcommand(self, msg):
return self.on_subcommand_show(msg, '')

def on_empty_subcmdhelp(self, msg):
return ('[\x02add\x02 \x1Fdice\x1F | '
'\x02clear\x02 | '
'\x02new\x02 \x1Fdice\x1F | '
'\x02roll\x02 \x1Fdice\x1F | '
'\x02show\x02 [\x1Fnick\x1F] | '
'\x02use\x02 \x1Frolls\x1F] - '
'Manage your die bank. '
'For more details on a specific subcommand, see help '
'for \x02{0}\x02 \x1Fsubcommand\x1F. '
'For information on dice notation, see help for '
'\x02{0} notation\x02.').format(msg.subaction)

def on_subcommand_add(self, msg, args):
return self.reply_for_roll(msg, args, update=True)

def on_subcmdhelp_add(self, msg):
return ('\x1Fdice\x1F - Roll the given dice and add the '
'resulting rolls to your die bank.')

def on_subcommand_clear(self, msg, args):
nick = msg.connection._lower(msg.actor.nick)
self.banks.pop((msg.venue, nick), None)
return 'Bank cleared.'

def on_subcmdhelp_clear(self, msg):
return '- Remove all rolls from your die bank.'

def on_subcommand_new(self, msg, args):
return self.reply_for_roll(msg, args, update=True, clear=True)

def on_subcmdhelp_new(self, msg):
return ('\x1Fdice\x1F - Remove all rolls from your die bank, '
'then roll the given dice and add the resulting rolls '
'to your die bank.')

def on_subcmdhelp_notation(self, msg):
return ('- Indicate dice using the standard '
'\x1FA\x1F\x02d\x02\x1FX\x1F notation, where \x1FA\x1F '
'is the number of dice to roll and \x1FX\x1F is the '
'die size. '
'Separate multiple sets of dice with spaces. '
'Positive integers may also be used as dice; they '
'"roll" to themselves.')

def on_subcommand_roll(self, msg, args):
return self.reply_for_roll(msg, args)

def on_subcmdhelp_roll(self, msg):
return ('\x1Fdice\x1F - Roll the given dice without adding the '
' resulting rolls to your die bank.')

def on_subcommand_show(self, msg, args):
nick = msg.connection._lower(args or msg.actor.nick)
rolls = self.banks[(msg.venue, nick)].elements()
return 'Bank has {}.'.format(format_rolls(rolls))

def on_subcmdhelp_show(self, msg):
return ('[\x1Fnick\x1F] - Show the rolls in the die bank '
'belonging to the user with the given nick, or your '
'own if no nick is provided.')

def on_subcommand_use(self, msg, args):
nick = msg.connection._lower(msg.actor.nick)
if not args:
raise UserVisibleError('Please specify rolls to use.')
rolls = []
for roll in args.split():
try:
rolls = self.roll_dice(args[1].split())
except ValueError as e:
raise UserVisibleError(str(e))
message = 'Rolled {}.'.format(format_rolls(rolls))
if subcommand in ('add', 'new'):
if subcommand == 'new':
self.banks.pop((msg.venue, nick), None)
bank = self.banks[(msg.venue, nick)]
bank.update(rolls)
message += ' Bank now has {}.'.format(
format_rolls(bank.elements()))
return message
if subcommand == 'use':
if len(args) < 2:
raise UserVisibleError('Please specify rolls to use.')
rolls = []
for roll in args[1].split():
try:
rolls.append(int(roll))
except ValueError:
raise UserVisibleError(
'{} is not a valid roll.'.format(roll))
# Figure out if the specified rolls actually exist by
# duplicating the bank, subtracting the rolls from it,
# and bailing if any of the counts are negative.
new_bank = Counter(self.banks[(msg.venue, nick)])
new_bank.subtract(rolls)
negatives = sorted([
roll for roll, count in new_bank.iteritems() if count < 0])
if negatives:
raise UserVisibleError(
'You do not have enough {} in your die bank to use '
'those rolls.'.format(
andify(['{}s'.format(n) for n in negatives])))
self.banks[(msg.venue, nick)] = new_bank
return 'Used {}. Bank now has {}.'.format(
format_rolls(rolls),
format_rolls(new_bank.elements()))
if subcommand == 'clear':
self.banks.pop((msg.venue, nick), None)
return 'Bank cleared.'
raise UserVisibleError(
'Unrecognized subcommand \x02{}\x02.'.format(subcommand))
rolls.append(int(roll))
except ValueError:
raise UserVisibleError('{} is not a valid roll.'.format(roll))
# Figure out if the specified rolls actually exist by
# duplicating the bank, subtracting the rolls from it,
# and bailing if any of the counts are negative.
new_bank = Counter(self.banks[(msg.venue, nick)])
new_bank.subtract(rolls)
negatives = sorted([
roll for roll, count in new_bank.iteritems() if count < 0])
if negatives:
raise UserVisibleError(
'You do not have enough {} in your die bank to use '
'those rolls.'.format(
andify(['{}s'.format(n) for n in negatives])))
self.banks[(msg.venue, nick)] = new_bank
return 'Used {}. Bank now has {}.'.format(
format_rolls(rolls),
format_rolls(new_bank.elements()))

def on_subcmdhelp_use(self, msg):
return '\x1Frolls\x1F - Remove the given rolls from your die bank.'

def on_nick(self, msg):
venues = [venue for venue, nick in self.banks
Expand All @@ -132,61 +185,3 @@ def on_nick(self, msg):
for venue in venues:
self.banks[(venue, new_nick)] = self.banks[(venue, old_nick)]
del self.banks[(venue, old_nick)]

def on_cmdhelp(self, msg):
if msg.content == 'add':
help_text = """\
\x02{1}\x02 \x1Fdice\x1F - Roll the given dice and add
the resulting rolls to your die bank.
"""
elif msg.content == 'clear':
help_text = """\
\x02{1}\x02 - Remove all rolls from your die bank.
"""
elif msg.content == 'new':
help_text = """\
\x02{1}\x02 \x1Fdice\x1F - Remove all rolls from your
die bank, then roll the given dice and add the resulting
rolls to your die bank.
"""
elif msg.content == 'notation':
help_text = """\
notation - Indicate dice using the standard
\x1FA\x1F\x02d\x02\x1FX\x1F notation, where
\x1FA\x1F is the number of dice to roll and
\x1FX\x1F is the die size.
Separate multiple sets of dice with spaces.
Positive integers may also be used as dice;
they "roll" to themselves.
"""
elif msg.content == 'roll':
help_text = """\
\x02{1}\x02 \x1Fdice\x1F - Roll the given dice without
adding the resulting rolls to your die bank.
"""
elif msg.content == 'show':
help_text = """\
\x02{1}\x02 [\x1Fnick\x1F] - Show the rolls in the die
bank belonging to the user with the given nick, or your
own if no nick is provided.'
"""
elif msg.content == 'use':
help_text = """\
\x02{1}\x02 \x1Frolls\x1F - Remove the given rolls from
your die bank.
"""
else:
help_text = """\
[\x02add\x02 \x1Fdice\x1F |
\x02clear\x02 |
\x02new\x02 \x1Fdice\x1F |
\x02roll\x02 \x1Fdice\x1F |
\x02show\x02 [\x1Fnick\x1F] |
\x02use\x02 \x1Frolls\x1F] -
Manage your die bank.
For more details on a specific subcommand, see help for
\x02{0}\x02 \x1Fsubcommand\x1F.
For information on dice notation, see help for \x02{0}
notation\x02.
"""
return collapse(help_text).format(msg.subaction, msg.content)

0 comments on commit f6fe808

Please sign in to comment.