@@ -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 | |||
@@ -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 | |||
<earwigbot.irc.data.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) | |||
<earwigbot.tasks.BaseTask.make_summary>` 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 | |||
@@ -36,9 +36,13 @@ class BaseCommand(object): | |||
This docstring is reported to the user when they type ``"!help | |||
<command>"``. | |||
""" | |||
# 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 | |||
<earwigbot.irc.data.Data.command>` ``==`` :py:attr:`self.name <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 <earwigbot.irc.data.Data.command>` is in | |||
:py:attr:`self.commands <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): | |||
@@ -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(' '): | |||
@@ -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" | |||
@@ -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"]: | |||
@@ -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": | |||
@@ -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": | |||
@@ -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: | |||
@@ -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.") | |||
@@ -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 <command>'." | |||
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): | |||
@@ -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: | |||
@@ -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": | |||
@@ -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"]: | |||
@@ -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: | |||
@@ -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) |
@@ -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: | |||
@@ -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)) | |||
@@ -0,0 +1,70 @@ | |||
# -*- coding: utf-8 -*- | |||
# | |||
# Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
# | |||
# 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) |
@@ -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 <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): | |||