diff --git a/README.rst b/README.rst index f14c1d5..2513903 100644 --- a/README.rst +++ b/README.rst @@ -166,6 +166,13 @@ for and what they should be overridden with, but these are the basics: - Class attribute ``name`` is the name of the command. This must be specified. +- Class attribute ``commands`` is a list of names that will trigger this + command. It defaults to the command's ``name``, but you can override it with + multiple names to serve as aliases. This is handled by the default + ``check()`` implementation (see below), so if ``check()`` is overridden, this + is ignored by everything except the help_ command (so ``!help alias`` will + trigger help for the actual command). + - Class attribute ``hooks`` is a list of the "IRC events" that this command might respond to. It defaults to ``["msg"]``, but options include ``"msg_private"`` (for private messages only), ``"msg_public"`` (for channel @@ -181,10 +188,12 @@ for and what they should be overridden with, but these are the basics: - Method ``check()`` is passed a ``Data`` [2]_ object, and should return ``True`` if you want to respond to this message, or ``False`` otherwise. The default behavior is to return ``True`` only if ``data.is_command`` is - ``True`` and ``data.command == self.name``, which is suitable for most cases. - A common, straightforward reason for overriding is if a command has aliases - (see chanops_ for an example). Note that by returning ``True``, you prevent - any other commands from responding to this message. + ``True`` and ``data.command`` ``==`` ``self.name`` (or ``data.command`` is in + ``self.commands`` if that list is overriden; see above), which is suitable + for most cases. A possible reason for overriding is if you want to do + something in response to events from a specific channel only. Note that by + returning ``True``, you prevent any other commands from responding to this + message. - Method ``process()`` is passed the same ``Data`` object as ``check()``, but only if ``check()`` returned ``True``. This is where the bulk of your command @@ -230,7 +239,7 @@ and what they should be overridden with, but these are the basics: ``"([[WP:BOT|Bot]]; [[User:EarwigBot#Task $1|Task $1]]): $2"``, which the task class's ``make_summary(comment)`` method will take and replace ``$1`` with the task number and ``$2`` with the details of the edit. - + Additionally, ``shutoff_enabled()`` (which checks whether the bot has been told to stop on-wiki by checking the content of a particular page) can check a different page for each task using similar variables. EarwigBot's @@ -510,6 +519,7 @@ Footnotes .. _earwigbot.bot.Bot: https://github.com/earwig/earwigbot/blob/develop/earwigbot/bot.py .. _earwigbot.config.BotConfig: https://github.com/earwig/earwigbot/blob/develop/earwigbot/config.py .. _earwigbot.commands.BaseCommand: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/__init__.py +.. _help: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/help.py .. _afc_status: https://github.com/earwig/earwigbot-plugins/blob/develop/commands/afc_status.py .. _chanops: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/chanops.py .. _test: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/test.py diff --git a/docs/customizing.rst b/docs/customizing.rst index f5b27f4..4891a22 100644 --- a/docs/customizing.rst +++ b/docs/customizing.rst @@ -96,6 +96,15 @@ these are the basics: - Class attribute :py:attr:`~earwigbot.commands.BaseCommand.name` is the name of the command. This must be specified. +- Class attribute :py:attr:`~earwigbot.commands.BaseCommand.commands` is a list + of names that will trigger this command. It defaults to the command's + :py:attr:`~earwigbot.commands.BaseCommand.name`, but you can override it with + multiple names to serve as aliases. This is handled by the default + :py:meth:`~earwigbot.commands.BaseCommand.check` implementation (see below), + so if :py:meth:`~earwigbot.commands.BaseCommand.check` is overridden, this is + ignored by everything except the help_ command (so ``!help alias`` will + trigger help for the actual command). + - Class attribute :py:attr:`~earwigbot.commands.BaseCommand.hooks` is a list of the "IRC events" that this command might respond to. It defaults to ``["msg"]``, but options include ``"msg_private"`` (for private messages @@ -113,12 +122,15 @@ these are the basics: - Method :py:meth:`~earwigbot.commands.BaseCommand.check` is passed a :py:class:`~earwigbot.irc.data.Data` [1]_ object, and should return ``True`` if you want to respond to this message, or ``False`` otherwise. The default - behavior is to return ``True`` only if - :py:attr:`data.is_command` is ``True`` and :py:attr:`data.command` == - :py:attr:`~earwigbot.commands.BaseCommand.name`, which is suitable for most - cases. A common, straightforward reason for overriding is if a command has - aliases (see chanops_ for an example). Note that by returning ``True``, you - prevent any other commands from responding to this message. + behavior is to return ``True`` only if :py:attr:`data.is_command` is ``True`` + and :py:attr:`data.command` ``==`` + :py:attr:`~earwigbot.commands.BaseCommand.name` (or :py:attr:`data.command + ` is in + :py:attr:`~earwigbot.commands.BaseCommand.commands` if that list is + overriden; see above), which is suitable for most cases. A possible reason + for overriding is if you want to do something in response to events from a + specific channel only. Note that by returning ``True``, you prevent any other + commands from responding to this message. - Method :py:meth:`~earwigbot.commands.BaseCommand.process` is passed the same :py:class:`~earwigbot.irc.data.Data` object as @@ -179,7 +191,7 @@ are the basics: task class's :py:meth:`make_summary(comment) ` method will take and replace ``$1`` with the task number and ``$2`` with the details of the edit. - + Additionally, :py:meth:`~earwigbot.tasks.BaseTask.shutoff_enabled` (which checks whether the bot has been told to stop on-wiki by checking the content of a particular page) can check a different page for each task using similar @@ -249,6 +261,7 @@ task, or the afc_statistics_ plugin for a more complicated one. :py:attr:`~earwigbot.irc.data.Data.ident`, and :py:attr:`~earwigbot.irc.data.Data.host`. +.. _help: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/help.py .. _afc_status: https://github.com/earwig/earwigbot-plugins/blob/develop/commands/afc_status.py .. _chanops: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/chanops.py .. _test: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/test.py diff --git a/earwigbot/commands/__init__.py b/earwigbot/commands/__init__.py index e3ba449..c072ca7 100644 --- a/earwigbot/commands/__init__.py +++ b/earwigbot/commands/__init__.py @@ -36,9 +36,13 @@ class BaseCommand(object): This docstring is reported to the user when they type ``"!help "``. """ - # This is the command's name, as reported to the user when they use !help: + # The command's name, as reported to the user when they use !help: name = None + # A list of names that will trigger this command. If left empty, it will + # be triggered by the command's name and its name only: + commands = [] + # Hooks are "msg", "msg_private", "msg_public", and "join". "msg" is the # default behavior; if you wish to override that, change the value in your # command subclass: @@ -86,11 +90,15 @@ class BaseCommand(object): sent on IRC, it should be cheap to execute and unlikely to throw exceptions. - Most commands return ``True`` if :py:attr:`data.command + Most commands return ``True`` only if :py:attr:`data.command ` ``==`` :py:attr:`self.name `, - otherwise they return ``False``. This is the default behavior of - :py:meth:`check`; you need only override it if you wish to change that. + or :py:attr:`data.command ` is in + :py:attr:`self.commands ` if that list is overriden. This is + the default behavior; you should only override it if you wish to change + that. """ + if self.commands: + return data.is_command and data.command in self.commands return data.is_command and data.command == self.name def process(self, data): diff --git a/earwigbot/commands/_old.py b/earwigbot/commands/_old.py index a4e8fc1..e22d681 100644 --- a/earwigbot/commands/_old.py +++ b/earwigbot/commands/_old.py @@ -21,11 +21,6 @@ def parse(command, line, line2, nick, chan, host, auth, notice, say, reply, s): u.close() say('"' + info['Date'] + '" - tycho.usno.navy.mil', chan) return - if command == "beats": - beats = ((time.time() + 3600) % 86400) / 86.4 - beats = int(math.floor(beats)) - say('@%03i' % beats, chan) - return if command == "dict" or command == "dictionary": def trim(thing): if thing.endswith(' '): diff --git a/earwigbot/commands/afc_pending.py b/earwigbot/commands/afc_pending.py index 1a43786..98b4dfb 100644 --- a/earwigbot/commands/afc_pending.py +++ b/earwigbot/commands/afc_pending.py @@ -25,10 +25,7 @@ from earwigbot.commands import BaseCommand class Command(BaseCommand): """Link the user to the pending AFC submissions page and category.""" name = "pending" - - def check(self, data): - commands = ["pending", "pend"] - return data.is_command and data.command in commands + commands = ["pending", "pend"] def process(self, data): msg1 = "pending submissions status page: http://enwp.org/WP:AFC/ST" diff --git a/earwigbot/commands/afc_status.py b/earwigbot/commands/afc_status.py index 635d6cc..a5aa228 100644 --- a/earwigbot/commands/afc_status.py +++ b/earwigbot/commands/afc_status.py @@ -28,13 +28,12 @@ class Command(BaseCommand): """Get the number of pending AfC submissions, open redirect requests, and open file upload requests.""" name = "status" + commands = ["status", "count", "num", "number"] hooks = ["join", "msg"] def check(self, data): - commands = ["status", "count", "num", "number"] - if data.is_command and data.command in commands: + if data.is_command and data.command in self.commands: return True - try: if data.line[1] == "JOIN" and data.chan == "#wikipedia-en-afc": if data.nick != self.config.irc["frontend"]["nick"]: diff --git a/earwigbot/commands/chanops.py b/earwigbot/commands/chanops.py index 1083a45..b2bcbca 100644 --- a/earwigbot/commands/chanops.py +++ b/earwigbot/commands/chanops.py @@ -26,10 +26,7 @@ class Command(BaseCommand): """Voice, devoice, op, or deop users in the channel, or join or part from other channels.""" name = "chanops" - - def check(self, data): - cmnds = ["chanops", "voice", "devoice", "op", "deop", "join", "part"] - return data.is_command and data.command in cmnds + commands = ["chanops", "voice", "devoice", "op", "deop", "join", "part"] def process(self, data): if data.command == "chanops": diff --git a/earwigbot/commands/crypt.py b/earwigbot/commands/crypt.py index ccb5300..611cde1 100644 --- a/earwigbot/commands/crypt.py +++ b/earwigbot/commands/crypt.py @@ -30,10 +30,7 @@ class Command(BaseCommand): """Provides hash functions with !hash (!hash list for supported algorithms) and blowfish encryption with !encrypt and !decrypt.""" name = "crypt" - - def check(self, data): - commands = ["crypt", "hash", "encrypt", "decrypt"] - return data.is_command and data.command in commands + commands = ["crypt", "hash", "encrypt", "decrypt"] def process(self, data): if data.command == "crypt": diff --git a/earwigbot/commands/editcount.py b/earwigbot/commands/editcount.py index 517a8e3..60f225d 100644 --- a/earwigbot/commands/editcount.py +++ b/earwigbot/commands/editcount.py @@ -28,10 +28,7 @@ from earwigbot.commands import BaseCommand class Command(BaseCommand): """Return a user's edit count.""" name = "editcount" - - def check(self, data): - commands = ["ec", "editcount"] - return data.is_command and data.command in commands + commands = ["ec", "editcount"] def process(self, data): if not data.args: diff --git a/earwigbot/commands/geolocate.py b/earwigbot/commands/geolocate.py index 67ba7c8..371e73c 100644 --- a/earwigbot/commands/geolocate.py +++ b/earwigbot/commands/geolocate.py @@ -28,6 +28,7 @@ from earwigbot.commands import BaseCommand class Command(BaseCommand): """Geolocate an IP address (via http://ipinfodb.com/).""" name = "geolocate" + commands = ["geolocate", "locate", "geo", "ip"] def setup(self): self.config.decrypt(self.config.commands, (self.name, "apiKey")) @@ -38,10 +39,6 @@ class Command(BaseCommand): log = 'Cannot use without an API key for http://ipinfodb.com/ stored as config.commands["{0}"]["apiKey"]' self.logger.warn(log.format(self.name)) - def check(self, data): - commands = ["geolocate", "locate", "geo", "ip"] - return data.is_command and data.command in commands - def process(self, data): if not data.args: self.reply(data, "please specify an IP to lookup.") diff --git a/earwigbot/commands/help.py b/earwigbot/commands/help.py index 35f97fc..e44cd51 100644 --- a/earwigbot/commands/help.py +++ b/earwigbot/commands/help.py @@ -23,7 +23,6 @@ import re from earwigbot.commands import BaseCommand -from earwigbot.irc import Data class Command(BaseCommand): """Displays help information.""" @@ -48,34 +47,24 @@ class Command(BaseCommand): def do_main_help(self, data): """Give the user a general help message with a list of all commands.""" msg = "Hi, I'm a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help '." - cmnds = sorted(self.bot.commands) + cmnds = sorted([cmnd.name for cmnd in self.bot.commands]) msg = msg.format(len(cmnds), ', '.join(cmnds)) self.reply(data, msg) def do_command_help(self, data): """Give the user help for a specific command.""" - command = data.args[0] + target = data.args[0] - # Create a dummy message to test which commands pick up the user's - # input: - msg = ":foo!bar@example.com PRIVMSG #channel :msg".split() - dummy = Data(self.bot, msg) - dummy.command = command.lower() - dummy.is_command = True + for command in self.bot.commands: + if command.name == target or target in command.commands: + if command.__doc__: + doc = command.__doc__.replace("\n", "") + doc = re.sub("\s\s+", " ", doc) + msg = "help for command \x0303{0}\x0301: \"{1}\"" + self.reply(data, msg.format(target, doc)) + return - for cmnd_name in self.bot.commands: - cmnd = self.bot.commands.get(cmnd_name) - if not cmnd.check(dummy): - continue - if cmnd.__doc__: - doc = cmnd.__doc__.replace("\n", "") - doc = re.sub("\s\s+", " ", doc) - msg = "help for command \x0303{0}\x0301: \"{1}\"" - self.reply(data, msg.format(command, doc)) - return - break - - msg = "sorry, no help for \x0303{0}\x0301.".format(command) + msg = "sorry, no help for \x0303{0}\x0301.".format(target) self.reply(data, msg) def do_hello(self, data): diff --git a/earwigbot/commands/langcode.py b/earwigbot/commands/langcode.py index 6695980..434e72b 100644 --- a/earwigbot/commands/langcode.py +++ b/earwigbot/commands/langcode.py @@ -26,10 +26,7 @@ class Command(BaseCommand): """Convert a language code into its name and a list of WMF sites in that language.""" name = "langcode" - - def check(self, data): - commands = ["langcode", "lang", "language"] - return data.is_command and data.command in commands + commands = ["langcode", "lang", "language"] def process(self, data): if not data.args: diff --git a/earwigbot/commands/praise.py b/earwigbot/commands/praise.py index e04fd3e..bb60801 100644 --- a/earwigbot/commands/praise.py +++ b/earwigbot/commands/praise.py @@ -25,11 +25,8 @@ from earwigbot.commands import BaseCommand class Command(BaseCommand): """Praise people!""" name = "praise" - - def check(self, data): - commands = ["praise", "earwig", "leonard", "leonard^bloom", "groove", - "groovedog"] - return data.is_command and data.command in commands + commands = ["praise", "earwig", "leonard", "leonard^bloom", "groove", + "groovedog"] def process(self, data): if data.command == "earwig": diff --git a/earwigbot/commands/quit.py b/earwigbot/commands/quit.py index 044f7ff..1bf538d 100644 --- a/earwigbot/commands/quit.py +++ b/earwigbot/commands/quit.py @@ -26,10 +26,7 @@ class Command(BaseCommand): """Quit, restart, or reload components from the bot. Only the owners can run this command.""" name = "quit" - - def check(self, data): - commands = ["quit", "restart", "reload"] - return data.is_command and data.command in commands + commands = ["quit", "restart", "reload"] def process(self, data): if data.host not in self.config.irc["permissions"]["owners"]: diff --git a/earwigbot/commands/registration.py b/earwigbot/commands/registration.py index 3ef641c..88c4119 100644 --- a/earwigbot/commands/registration.py +++ b/earwigbot/commands/registration.py @@ -28,10 +28,7 @@ from earwigbot.commands import BaseCommand class Command(BaseCommand): """Return when a user registered.""" name = "registration" - - def check(self, data): - commands = ["registration", "reg", "age"] - return data.is_command and data.command in commands + commands = ["registration", "reg", "age"] def process(self, data): if not data.args: diff --git a/earwigbot/commands/remind.py b/earwigbot/commands/remind.py index 4a7d6ed..b82faf8 100644 --- a/earwigbot/commands/remind.py +++ b/earwigbot/commands/remind.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import threading +from threading import Timer import time from earwigbot.commands import BaseCommand @@ -28,9 +28,7 @@ from earwigbot.commands import BaseCommand class Command(BaseCommand): """Set a message to be repeated to you in a certain amount of time.""" name = "remind" - - def check(self, data): - return data.is_command and data.command in ["remind", "reminder"] + commands = ["remind", "reminder"] def process(self, data): if not data.args: @@ -58,12 +56,7 @@ class Command(BaseCommand): msg = msg.format(message, wait, end_time_with_timezone) self.reply(data, msg) - t_reminder = threading.Thread(target=self.reminder, - args=(data, message, wait)) + t_reminder = Timer(wait, self.reply, args=(data, message)) t_reminder.name = "reminder " + end_time t_reminder.daemon = True t_reminder.start() - - def reminder(self, data, message, wait): - time.sleep(wait) - self.reply(data, message) diff --git a/earwigbot/commands/rights.py b/earwigbot/commands/rights.py index 63357c7..bcd1c10 100644 --- a/earwigbot/commands/rights.py +++ b/earwigbot/commands/rights.py @@ -26,10 +26,7 @@ from earwigbot.commands import BaseCommand class Command(BaseCommand): """Retrieve a list of rights for a given username.""" name = "rights" - - def check(self, data): - commands = ["rights", "groups", "permissions", "privileges"] - return data.is_command and data.command in commands + commands = ["rights", "groups", "permissions", "privileges"] def process(self, data): if not data.args: diff --git a/earwigbot/commands/threads.py b/earwigbot/commands/threads.py index e1bbb22..62cb60c 100644 --- a/earwigbot/commands/threads.py +++ b/earwigbot/commands/threads.py @@ -29,10 +29,7 @@ from earwigbot.exceptions import KwargParseError class Command(BaseCommand): """Manage wiki tasks from IRC, and check on thread status.""" name = "threads" - - def check(self, data): - commands = ["tasks", "task", "threads", "tasklist"] - return data.is_command and data.command in commands + commands = ["tasks", "task", "threads", "tasklist"] def process(self, data): self.data = data @@ -103,7 +100,7 @@ class Command(BaseCommand): whether they are currently running or idle.""" threads = threading.enumerate() tasklist = [] - for task in sorted(self.bot.tasks): + for task in sorted([task.name for task in self.bot.tasks]): threadlist = [t for t in threads if t.name.startswith(task)] ids = [str(t.ident) for t in threadlist] if not ids: @@ -138,7 +135,7 @@ class Command(BaseCommand): self.reply(data, msg) return - if task_name not in self.bot.tasks: + if task_name not in [task.name for task in self.bot.tasks]: # This task does not exist or hasn't been loaded: msg = "task could not be found; either it doesn't exist, or it wasn't loaded correctly." self.reply(data, msg.format(task_name)) diff --git a/earwigbot/commands/time.py b/earwigbot/commands/time.py new file mode 100644 index 0000000..dae1822 --- /dev/null +++ b/earwigbot/commands/time.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2012 by Ben Kurtovic +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from datetime import datetime, timedelta +from math import floor +from time import time + +from earwigbot.commands import BaseCommand + +class Command(BaseCommand): + """Report the current time in any timezone (UTC default), or in beats.""" + name = "time" + commands = ["time", "beats", "swatch"] + timezones = [ + "UTC": 0, + "EST": -5, + "EDT": -4, + "CST": -6, + "CDT": -5, + "MST": -7, + "MDT": -6, + "PST": -8, + "PDT": -7, + ] + + def process(self, data): + if data.command in ["beats", "swatch"]: + self.do_beats(data) + return + if data.args: + timezone = data.args[0] + else: + timezone = "UTC" + if timezone in ["beats", "swatch"]: + self.do_beats(data) + else: + self.do_time(data, timezone) + + def do_beats(self, data): + beats = ((time() + 3600) % 86400) / 86.4 + beats = int(floor(beats)) + self.reply(data, "@{0:0>3}".format(beats)) + + def do_time(self, data, timezone): + now = datetime.utcnow() + try: + now += timedelta(hours=self.timezones[timezone]) # Timezone offset + except KeyError: + self.reply(data, "unknown timezone: {0}.".format(timezone)) + return + self.reply(data, now.strftime("%Y-%m-%d %H:%M:%S") + " " + timezone) diff --git a/earwigbot/managers.py b/earwigbot/managers.py index e7882f3..dd18b63 100644 --- a/earwigbot/managers.py +++ b/earwigbot/managers.py @@ -24,7 +24,7 @@ import imp from os import listdir, path from re import sub -from threading import Lock, Thread +from threading import RLock, Thread from time import gmtime, strftime from earwigbot.commands import BaseCommand @@ -46,11 +46,7 @@ class _ResourceManager(object): This class handles the low-level tasks of (re)loading resources via :py:meth:`load`, retrieving specific resources via :py:meth:`get`, and - iterating over all resources via :py:meth:`__iter__`. If iterating over - resources, it is recommended to acquire :py:attr:`self.lock ` - beforehand and release it afterwards (alternatively, wrap your code in a - ``with`` statement) so an attempt at reloading resources in another thread - won't disrupt your iteration. + iterating over all resources via :py:meth:`__iter__`. """ def __init__(self, bot, name, attribute, base): self.bot = bot @@ -60,16 +56,12 @@ class _ResourceManager(object): self._resource_name = name # e.g. "commands" or "tasks" self._resource_attribute = attribute # e.g. "Command" or "Task" self._resource_base = base # e.g. BaseCommand or BaseTask - self._resource_access_lock = Lock() - - @property - def lock(self): - """The resource access/modify lock.""" - return self._resource_access_lock + self._resource_access_lock = RLock() def __iter__(self): - for name in self._resources: - yield name + with self.lock: + for resource in self._resources.itervalues(): + yield resource def _load_resource(self, name, path): """Load a specific resource from a module, identified by name and path. @@ -118,6 +110,11 @@ class _ResourceManager(object): self._load_resource(modname, dir) processed.append(modname) + @property + def lock(self): + """The resource access/modify lock.""" + return self._resource_access_lock + def load(self): """Load (or reload) all valid resources into :py:attr:`_resources`.""" name = self._resource_name # e.g. "commands" or "tasks" @@ -138,7 +135,8 @@ class _ResourceManager(object): Will raise :py:exc:`KeyError` if the resource (a command or task) is not found. """ - return self._resources[key] + with self.lock: + return self._resources[key] class CommandManager(_ResourceManager): @@ -167,13 +165,10 @@ class CommandManager(_ResourceManager): def call(self, hook, data): """Respond to a hook type and a :py:class:`Data` object.""" - self.lock.acquire() - for command in self._resources.itervalues(): + for command in self: if hook in command.hooks and self._wrap_check(command, data): - self.lock.release() self._wrap_process(command, data) return - self.lock.release() class TaskManager(_ResourceManager):