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
=== modified file 'src/endroid/cron.py'
--- src/endroid/cron.py 2013-08-13 10:04:19 +0000
+++ src/endroid/cron.py 2013-08-21 16:08:24 +0000
@@ -4,25 +4,68 @@
4# Created by Jonathan Millican4# Created by Jonathan Millican
5# -----------------------------------------5# -----------------------------------------
66
7from endroid.database import Database as Database7from endroid.database import Database
8
9from twisted.internet import reactor
10from pytz import timezone
8import datetime11import datetime
9from twisted.internet import reactor
10import cPickle12import cPickle
11from pytz import timezone
12import logging13import logging
14import functools
15
16__all__ = (
17 'task',
18 'Cron',
19)
20
21
22def task(name, persistent=True):
23 """
24 Decorator for method of Plugin classes that are to be used as callbacks for
25 cron tasks.
26
27 This decorator ensures the function is registered with the plugin's cron
28 using the given name and persistent arguments. The decorated function can
29 be used like a Task (returned from Cron.register):
30
31 >>> CRON_NAME = "MyCronName"
32 >>> class Foo(Plugin):
33 ... @task(CRON_NAME)
34 ... def repeat(self, *args): pass
35 ... def message_handler(self, msg):
36 ... self.repeat.setTimeout(10, params)
37
38 That is it provides the setTimeout and doAtTime methods. The decorated
39 function can also be called as normal, should that be needed.
40
41 """
42 def decorator(fn):
43 fn._cron_iscb = True
44 fn._cron_name = name
45 fn._cron_persistent = persistent
46 return fn
47 return decorator
1348
1449
15class Task(object):50class Task(object):
16 """51 """
17 Wrapper object providing direct access to a specific "task". Obtain an52 Wrapper object providing direct access to a specific "task". Obtain an
18 instance using register on the Cron singleton.53 instance using register on the Cron singleton.
1954
55 Can also be obtained by wrapping a callback method using the @task
56 decorator. In either case, the Task object is updated to appear as if it is
57 the underlying function, and can be called as if it is.
58
20 """59 """
21 __slots__ = ('name', 'cron')60 def __init__(self, name, cron, fn):
22
23 def __init__(self, name, cron):
24 self.name = name61 self.name = name
25 self.cron = cron62 self.cron = cron
63 self.fn = fn
64 # Disguise the Task as the function it is wrapping
65 functools.update_wrapper(self, fn)
66
67 def __call__(self, *args, **kwargs):
68 return self.fn(*args, **kwargs)
2669
27 def doAtTime(self, time, locality, params):70 def doAtTime(self, time, locality, params):
28 return self.cron.doAtTime(time, locality, self.name, params)71 return self.cron.doAtTime(time, locality, self.name, params)
@@ -94,7 +137,7 @@
94 self.removeTask(reg_name)137 self.removeTask(reg_name)
95138
96 self.fun_dict.update({reg_name: function})139 self.fun_dict.update({reg_name: function})
97 return Task(reg_name, self)140 return Task(reg_name, self, function)
98141
99 def cancel(self):142 def cancel(self):
100 if self.delayedcall:143 if self.delayedcall:
101144
=== modified file 'src/endroid/pluginmanager.py'
--- src/endroid/pluginmanager.py 2013-08-14 18:18:15 +0000
+++ src/endroid/pluginmanager.py 2013-08-21 16:08:24 +0000
@@ -6,6 +6,8 @@
66
7import sys7import sys
8import logging8import logging
9import functools
10
9from endroid.cron import Cron11from endroid.cron import Cron
10from endroid.database import Database12from endroid.database import Database
1113
@@ -56,6 +58,26 @@
56 if 'enInit' in dict:58 if 'enInit' in dict:
57 dict['endroid_init'] = dict['enInit']59 dict['endroid_init'] = dict['enInit']
58 del dict['enInit']60 del dict['enInit']
61
62 # Support for the Cron @task decorator
63 crons = {name: fn for name, fn in dict.items()
64 if getattr(fn, "_cron_iscb", False)}
65 init = dict.get('endroid_init', lambda _: None)
66 # Make sure the new init function looks like the old one
67 @functools.wraps(init)
68 def endroid_init(self):
69 for name, fn in crons.items():
70 @functools.wraps(fn)
71 def inner(*args, **kwargs):
72 # This function is here to ensure the right obj is passed
73 # as self to the method.
74 return fn(self, *args, **kwargs)
75 task = self.cron.register(inner, fn._cron_name,
76 persistent=fn._cron_persistent)
77 setattr(self, name, task)
78 init(self)
79 dict['endroid_init'] = endroid_init
80
59 return type.__new__(meta, name, bases, dict)81 return type.__new__(meta, name, bases, dict)
6082
61 def __init__(cls, name, bases, dict):83 def __init__(cls, name, bases, dict):
6284
=== modified file 'src/endroid/plugins/command.py'
--- src/endroid/plugins/command.py 2013-08-13 18:38:51 +0000
+++ src/endroid/plugins/command.py 2013-08-21 16:08:24 +0000
@@ -4,6 +4,7 @@
4# Created by Jonathan Millican4# Created by Jonathan Millican
5# -----------------------------------------5# -----------------------------------------
66
7import functools
7from collections import namedtuple8from collections import namedtuple
8from endroid.pluginmanager import Plugin, PluginMeta9from endroid.pluginmanager import Plugin, PluginMeta
910
@@ -72,6 +73,8 @@
72 cmds = dict((c, f) for c, f in dct.items()73 cmds = dict((c, f) for c, f in dct.items()
73 if c.startswith("cmd_") or getattr(f, "is_command", False))74 if c.startswith("cmd_") or getattr(f, "is_command", False))
74 init = dct.get('endroid_init', lambda _: None)75 init = dct.get('endroid_init', lambda _: None)
76 # Make sure the new init function looks like the old one
77 @functools.wraps(init)
75 def endroid_init(self):78 def endroid_init(self):
76 com = self.get('endroid.plugins.command')79 com = self.get('endroid.plugins.command')
77 for cmd, fn in cmds.items():80 for cmd, fn in cmds.items():
@@ -82,6 +85,9 @@
82 else:85 else:
83 reg_fn = com.register_both86 reg_fn = com.register_both
84 words = cmd.split("_")87 words = cmd.split("_")
88 # Handle a leading underscore (may be necessary in some cases)
89 if not words[0]:
90 words = words[1:]
85 if not getattr(fn, "is_command", False):91 if not getattr(fn, "is_command", False):
86 words = words[1:]92 words = words[1:]
87 reg_fn(getattr(self, cmd), words,93 reg_fn(getattr(self, cmd), words,
8894
=== modified file 'src/endroid/plugins/help.py'
--- src/endroid/plugins/help.py 2013-08-11 19:50:24 +0000
+++ src/endroid/plugins/help.py 2013-08-21 16:08:24 +0000
@@ -3,9 +3,11 @@
3# Copyright 2012, Ensoft Ltd3# Copyright 2012, Ensoft Ltd
4# -----------------------------------------------------------------------------4# -----------------------------------------------------------------------------
55
6from endroid.plugins.command import CommandPlugin6from endroid.plugins.command import CommandPlugin, command
77
8class Help(CommandPlugin):8class Help(CommandPlugin):
9 name = "help"
10
9 def endroid_init(self):11 def endroid_init(self):
10 self.help_topics = {12 self.help_topics = {
11 '': self.show_help_main,13 '': self.show_help_main,
@@ -19,12 +21,13 @@
1921
20 def load_plugin_list(self):22 def load_plugin_list(self):
21 self._plugins = {}23 self._plugins = {}
22 for fullname in self.list_plugins():24 for fullname in self.plugins.all():
23 plugin = self.get(fullname)25 plugin = self.plugins.get(fullname)
24 name = getattr(plugin, "name", fullname)26 name = getattr(plugin, "name", fullname)
25 self._plugins[name] = (fullname, plugin)27 self._plugins[name] = (fullname, plugin)
2628
27 def cmd_help(self, msg, args):29 @command
30 def _help(self, msg, args):
28 msg.reply_to_sender(self.show_help_plugin("help", args))31 msg.reply_to_sender(self.show_help_plugin("help", args))
2932
30 def show_help_main(self, topic):33 def show_help_main(self, topic):
@@ -54,7 +57,7 @@
54 def show_help_plugin(self, name, topic=''):57 def show_help_plugin(self, name, topic=''):
55 out = []58 out = []
56 fullname, plugin = self._plugins.get(name, (name, None))59 fullname, plugin = self._plugins.get(name, (name, None))
57 if self.pluginLoaded(fullname):60 if self.plugins.loaded(fullname):
58 # First check if it has a simple "help" property or method61 # First check if it has a simple "help" property or method
59 if hasattr(plugin, "help"):62 if hasattr(plugin, "help"):
60 try:63 try:
6164
=== modified file 'src/endroid/plugins/ratelimit.py'
--- src/endroid/plugins/ratelimit.py 2013-08-14 18:18:15 +0000
+++ src/endroid/plugins/ratelimit.py 2013-08-21 16:08:24 +0000
@@ -11,6 +11,7 @@
1111
12from endroid.pluginmanager import Plugin12from endroid.pluginmanager import Plugin
13from endroid.messagehandler import Priority13from endroid.messagehandler import Priority
14from endroid.cron import task
1415
15# Cron constants16# Cron constants
16CRON_SENDAGAIN = "RateLimit_SendAgain"17CRON_SENDAGAIN = "RateLimit_SendAgain"
@@ -178,8 +179,6 @@
178179
179 RateLimit.waitingusers = set()180 RateLimit.waitingusers = set()
180181
181 self.task = self.cron.register(self.sendagain, CRON_SENDAGAIN)
182
183 def ratelimit(self, msg):182 def ratelimit(self, msg):
184 """183 """
185 Send message filter. Rate limits based on the message recipient, using184 Send message filter. Rate limits based on the message recipient, using
@@ -189,7 +188,7 @@
189 messages. TODO: store queued messages in the DB so they survive a188 messages. TODO: store queued messages in the DB so they survive a
190 process restart?189 process restart?
191190
192 PRIORITY_URGENT messages are not rate limited.191 Priority.URGENT messages are not rate limited.
193 """192 """
194 sc = self.limiters[msg.recipient]193 sc = self.limiters[msg.recipient]
195194
@@ -229,6 +228,7 @@
229228
230 return True229 return True
231230
231 @task(CRON_SENDAGAIN)
232 def sendagain(self, user):232 def sendagain(self, user):
233 """233 """
234 Cron callback handler. Attempts to send as many messages as it can from234 Cron callback handler. Attempts to send as many messages as it can from
@@ -265,4 +265,4 @@
265 if len(sc.queue) > 0 and user not in self.waitingusers:265 if len(sc.queue) > 0 and user not in self.waitingusers:
266 self.waitingusers.add(user)266 self.waitingusers.add(user)
267 timedelta = 1.0 / self.fillrate267 timedelta = 1.0 / self.fillrate
268 self.task.setTimeout(timedelta, user)268 self.sendagain.setTimeout(timedelta, user)
269269
=== modified file 'src/endroid/usermanagement.py'
--- src/endroid/usermanagement.py 2013-08-19 15:06:08 +0000
+++ src/endroid/usermanagement.py 2013-08-21 16:08:24 +0000
@@ -603,19 +603,18 @@
603 """603 """
604 try:604 try:
605 return JID(item).userhost()605 return JID(item).userhost()
606 # will throw a RuntimeError if item is None606 # will raise a RuntimeError if item is None
607 # or an InvalidFormat if item is a string but not formatted properly607 # or an InvalidFormat if item is a string but not formatted properly
608 # If the item is not even a string, will raise attributeerror608 # If the item is not even a string, will raise AttributeError
609 except (RuntimeError, InvalidFormat, AttributeError):609 except (RuntimeError, InvalidFormat, AttributeError):
610 return item610 return item
611 # if item is not a string or None, an AttributeError will be raised
612611
613 def for_plugin(self, pluginmanager, plugin):612 def for_plugin(self, pluginmanager, plugin):
614 return PluginUserManagement(self, pluginmanager, plugin)613 return PluginUserManagement(self, pluginmanager, plugin)
615614
616class PluginUserManagement(object):615class PluginUserManagement(object):
617 """616 """
618 One of these exists per plugin, provide the API to handle rosters.617 One of these exists per plugin, provides the API to handle rosters.
619 """618 """
620 def __init__(self, usermanagement, pluginmanager, plugin):619 def __init__(self, usermanagement, pluginmanager, plugin):
621 self._usermanagement = usermanagement620 self._usermanagement = usermanagement

Subscribers

People subscribed via source and target branches