Merge lp:~isoschiz/endroid/crontaskdecorator into lp:endroid

Proposed by Martin Morrison
Status: Merged
Approved by: Phil Connell
Approved revision: 51
Merged at revision: 51
Proposed branch: lp:~isoschiz/endroid/crontaskdecorator
Merge into: lp:endroid
Diff against target: 273 lines (+94/-21)
6 files modified
src/endroid/cron.py (+51/-8)
src/endroid/pluginmanager.py (+22/-0)
src/endroid/plugins/command.py (+6/-0)
src/endroid/plugins/help.py (+8/-5)
src/endroid/plugins/ratelimit.py (+4/-4)
src/endroid/usermanagement.py (+3/-4)
To merge this branch: bzr merge lp:~isoschiz/endroid/crontaskdecorator
Reviewer Review Type Date Requested Status
Phil Connell Approve
ChrisD Needs Fixing
Review via email: mp+180739@code.launchpad.net

Commit message

Merge in new @task handler.

Description of the change

Adds a @task decorator in the cron module that can be used to decorate methods that are to be used as timed callback handlers. The decorator converts these into a Task object, via the use of a descriptor (which ensures the Task is created by the specific cron associated with the given plugin: for now, there is a single Cron, but this code is future proof for when we have per-plugin Crons).

Also has minor tweaks to help and ratelimit to use some new features.

To post a comment you must log in.
Revision history for this message
ChrisD (gingerchris) wrote :

What's with the lowercase d?
Also the decorator class could do with more comments.
Can you add comments around all functools calls as well?

Finally, I think this might break some usage of persistent crons because it appears to delay registration until the task is used (if I'm following correctly). This might mean after a restart that a plugin might not have re-registered a persistent cron by he time it fires.

Appreciate the other updates :)

review: Needs Fixing
Revision history for this message
Martin Morrison (isoschiz) wrote :

Lower case d is an artefact of the evolution, where it was originally a function. In this case, it doesn't really matter as it's all internal, but I'll change it. I can also add more comments.

Good catch on the deferred registration! Fixing that will require moving the support to the metaclass, which means this all gets a lot simpler anyway - probably no longer need the decorator class.

Revision history for this message
Phil Connell (pconnell) wrote :

There's an unrelated changeset in here (everything outside cron.py/pluginmanager.py). Is that being merged separately?

- Missing Task in cron.__all__?

- In the docstring for the task decorator, add ">>> CRON_NAME = 'foo'" to make it clear that it's a string?

- It looks a lot like you're missing a functools.wraps for the task decorator. [aside: would be nice if lp generated -p diffs...]
- Possibly a nicer solution to the above would be to move the update_wrapper call inside Task.__init__.

- Should the attributes set on the decorated fn have leading underscores? Since they're internal implementation details, I'd say they should. Would also be nice if they had a common prefix (e.g. "_cron") to make it obvious which to pick out in e.g. a vars() dump.

- name/fn rather than c/f in the dict comprehension? The correspondence with looping over crons in the new endroid_init would then be more obvious.

review: Approve
51. By Martin Morrison

Markups following review

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/endroid/cron.py'
2--- src/endroid/cron.py 2013-08-13 10:04:19 +0000
3+++ src/endroid/cron.py 2013-08-21 16:08:24 +0000
4@@ -4,25 +4,68 @@
5 # Created by Jonathan Millican
6 # -----------------------------------------
7
8-from endroid.database import Database as Database
9+from endroid.database import Database
10+
11+from twisted.internet import reactor
12+from pytz import timezone
13 import datetime
14-from twisted.internet import reactor
15 import cPickle
16-from pytz import timezone
17 import logging
18+import functools
19+
20+__all__ = (
21+ 'task',
22+ 'Cron',
23+)
24+
25+
26+def task(name, persistent=True):
27+ """
28+ Decorator for method of Plugin classes that are to be used as callbacks for
29+ cron tasks.
30+
31+ This decorator ensures the function is registered with the plugin's cron
32+ using the given name and persistent arguments. The decorated function can
33+ be used like a Task (returned from Cron.register):
34+
35+ >>> CRON_NAME = "MyCronName"
36+ >>> class Foo(Plugin):
37+ ... @task(CRON_NAME)
38+ ... def repeat(self, *args): pass
39+ ... def message_handler(self, msg):
40+ ... self.repeat.setTimeout(10, params)
41+
42+ That is it provides the setTimeout and doAtTime methods. The decorated
43+ function can also be called as normal, should that be needed.
44+
45+ """
46+ def decorator(fn):
47+ fn._cron_iscb = True
48+ fn._cron_name = name
49+ fn._cron_persistent = persistent
50+ return fn
51+ return decorator
52
53
54 class Task(object):
55 """
56- Wrapper object providing direct access to a specific "task". Obtain an
57+ Wrapper object providing direct access to a specific "task". Obtain an
58 instance using register on the Cron singleton.
59
60+ Can also be obtained by wrapping a callback method using the @task
61+ decorator. In either case, the Task object is updated to appear as if it is
62+ the underlying function, and can be called as if it is.
63+
64 """
65- __slots__ = ('name', 'cron')
66-
67- def __init__(self, name, cron):
68+ def __init__(self, name, cron, fn):
69 self.name = name
70 self.cron = cron
71+ self.fn = fn
72+ # Disguise the Task as the function it is wrapping
73+ functools.update_wrapper(self, fn)
74+
75+ def __call__(self, *args, **kwargs):
76+ return self.fn(*args, **kwargs)
77
78 def doAtTime(self, time, locality, params):
79 return self.cron.doAtTime(time, locality, self.name, params)
80@@ -94,7 +137,7 @@
81 self.removeTask(reg_name)
82
83 self.fun_dict.update({reg_name: function})
84- return Task(reg_name, self)
85+ return Task(reg_name, self, function)
86
87 def cancel(self):
88 if self.delayedcall:
89
90=== modified file 'src/endroid/pluginmanager.py'
91--- src/endroid/pluginmanager.py 2013-08-14 18:18:15 +0000
92+++ src/endroid/pluginmanager.py 2013-08-21 16:08:24 +0000
93@@ -6,6 +6,8 @@
94
95 import sys
96 import logging
97+import functools
98+
99 from endroid.cron import Cron
100 from endroid.database import Database
101
102@@ -56,6 +58,26 @@
103 if 'enInit' in dict:
104 dict['endroid_init'] = dict['enInit']
105 del dict['enInit']
106+
107+ # Support for the Cron @task decorator
108+ crons = {name: fn for name, fn in dict.items()
109+ if getattr(fn, "_cron_iscb", False)}
110+ init = dict.get('endroid_init', lambda _: None)
111+ # Make sure the new init function looks like the old one
112+ @functools.wraps(init)
113+ def endroid_init(self):
114+ for name, fn in crons.items():
115+ @functools.wraps(fn)
116+ def inner(*args, **kwargs):
117+ # This function is here to ensure the right obj is passed
118+ # as self to the method.
119+ return fn(self, *args, **kwargs)
120+ task = self.cron.register(inner, fn._cron_name,
121+ persistent=fn._cron_persistent)
122+ setattr(self, name, task)
123+ init(self)
124+ dict['endroid_init'] = endroid_init
125+
126 return type.__new__(meta, name, bases, dict)
127
128 def __init__(cls, name, bases, dict):
129
130=== modified file 'src/endroid/plugins/command.py'
131--- src/endroid/plugins/command.py 2013-08-13 18:38:51 +0000
132+++ src/endroid/plugins/command.py 2013-08-21 16:08:24 +0000
133@@ -4,6 +4,7 @@
134 # Created by Jonathan Millican
135 # -----------------------------------------
136
137+import functools
138 from collections import namedtuple
139 from endroid.pluginmanager import Plugin, PluginMeta
140
141@@ -72,6 +73,8 @@
142 cmds = dict((c, f) for c, f in dct.items()
143 if c.startswith("cmd_") or getattr(f, "is_command", False))
144 init = dct.get('endroid_init', lambda _: None)
145+ # Make sure the new init function looks like the old one
146+ @functools.wraps(init)
147 def endroid_init(self):
148 com = self.get('endroid.plugins.command')
149 for cmd, fn in cmds.items():
150@@ -82,6 +85,9 @@
151 else:
152 reg_fn = com.register_both
153 words = cmd.split("_")
154+ # Handle a leading underscore (may be necessary in some cases)
155+ if not words[0]:
156+ words = words[1:]
157 if not getattr(fn, "is_command", False):
158 words = words[1:]
159 reg_fn(getattr(self, cmd), words,
160
161=== modified file 'src/endroid/plugins/help.py'
162--- src/endroid/plugins/help.py 2013-08-11 19:50:24 +0000
163+++ src/endroid/plugins/help.py 2013-08-21 16:08:24 +0000
164@@ -3,9 +3,11 @@
165 # Copyright 2012, Ensoft Ltd
166 # -----------------------------------------------------------------------------
167
168-from endroid.plugins.command import CommandPlugin
169+from endroid.plugins.command import CommandPlugin, command
170
171 class Help(CommandPlugin):
172+ name = "help"
173+
174 def endroid_init(self):
175 self.help_topics = {
176 '': self.show_help_main,
177@@ -19,12 +21,13 @@
178
179 def load_plugin_list(self):
180 self._plugins = {}
181- for fullname in self.list_plugins():
182- plugin = self.get(fullname)
183+ for fullname in self.plugins.all():
184+ plugin = self.plugins.get(fullname)
185 name = getattr(plugin, "name", fullname)
186 self._plugins[name] = (fullname, plugin)
187
188- def cmd_help(self, msg, args):
189+ @command
190+ def _help(self, msg, args):
191 msg.reply_to_sender(self.show_help_plugin("help", args))
192
193 def show_help_main(self, topic):
194@@ -54,7 +57,7 @@
195 def show_help_plugin(self, name, topic=''):
196 out = []
197 fullname, plugin = self._plugins.get(name, (name, None))
198- if self.pluginLoaded(fullname):
199+ if self.plugins.loaded(fullname):
200 # First check if it has a simple "help" property or method
201 if hasattr(plugin, "help"):
202 try:
203
204=== modified file 'src/endroid/plugins/ratelimit.py'
205--- src/endroid/plugins/ratelimit.py 2013-08-14 18:18:15 +0000
206+++ src/endroid/plugins/ratelimit.py 2013-08-21 16:08:24 +0000
207@@ -11,6 +11,7 @@
208
209 from endroid.pluginmanager import Plugin
210 from endroid.messagehandler import Priority
211+from endroid.cron import task
212
213 # Cron constants
214 CRON_SENDAGAIN = "RateLimit_SendAgain"
215@@ -178,8 +179,6 @@
216
217 RateLimit.waitingusers = set()
218
219- self.task = self.cron.register(self.sendagain, CRON_SENDAGAIN)
220-
221 def ratelimit(self, msg):
222 """
223 Send message filter. Rate limits based on the message recipient, using
224@@ -189,7 +188,7 @@
225 messages. TODO: store queued messages in the DB so they survive a
226 process restart?
227
228- PRIORITY_URGENT messages are not rate limited.
229+ Priority.URGENT messages are not rate limited.
230 """
231 sc = self.limiters[msg.recipient]
232
233@@ -229,6 +228,7 @@
234
235 return True
236
237+ @task(CRON_SENDAGAIN)
238 def sendagain(self, user):
239 """
240 Cron callback handler. Attempts to send as many messages as it can from
241@@ -265,4 +265,4 @@
242 if len(sc.queue) > 0 and user not in self.waitingusers:
243 self.waitingusers.add(user)
244 timedelta = 1.0 / self.fillrate
245- self.task.setTimeout(timedelta, user)
246+ self.sendagain.setTimeout(timedelta, user)
247
248=== modified file 'src/endroid/usermanagement.py'
249--- src/endroid/usermanagement.py 2013-08-19 15:06:08 +0000
250+++ src/endroid/usermanagement.py 2013-08-21 16:08:24 +0000
251@@ -603,19 +603,18 @@
252 """
253 try:
254 return JID(item).userhost()
255- # will throw a RuntimeError if item is None
256+ # will raise a RuntimeError if item is None
257 # or an InvalidFormat if item is a string but not formatted properly
258- # If the item is not even a string, will raise attributeerror
259+ # If the item is not even a string, will raise AttributeError
260 except (RuntimeError, InvalidFormat, AttributeError):
261 return item
262- # if item is not a string or None, an AttributeError will be raised
263
264 def for_plugin(self, pluginmanager, plugin):
265 return PluginUserManagement(self, pluginmanager, plugin)
266
267 class PluginUserManagement(object):
268 """
269- One of these exists per plugin, provide the API to handle rosters.
270+ One of these exists per plugin, provides the API to handle rosters.
271 """
272 def __init__(self, usermanagement, pluginmanager, plugin):
273 self._usermanagement = usermanagement

Subscribers

People subscribed via source and target branches