Commits

nakamura committed b93bc5d

init

  • Participants

Comments (0)

Files changed (15)

File examples/example.py

+import json
+
+from tsumuji import Application, \
+    getApplication, getRequest, urlFor, statefulCallbacks, returnValue
+
+
+app = Application()
+app.comments = []
+
+
+# you can write request handlers as a function,
+# and register it to URL pattern
+@app.route('/')
+def index():
+    return 'this is index page.\n\n'
+
+
+# statefulCallbacks can access `state`, such as request object.
+@app.route('/who')
+@statefulCallbacks
+def create_comment():
+    request = yield getRequest()
+    name = request.args['name'][0]
+
+    returnValue('your name is {0}'.format(name))
+
+
+# capture URL segment using <name:type> pattern
+@app.route('/who/<name:str>')
+def show_comment(name):
+    return 'your name is {0}'.format(name)
+
+
+from twisted.web import static
+
+# you can also put any IResource providers to URL
+app.route('/file')(static.File('.'))
+
+
+app.run(8080)

File examples/guestbook.py

+from tsumuji import Application, \
+    getApplication, getRequest, urlFor, \
+    statefulCallbacks, returnValue, writeTemplate, redirect
+
+
+app = Application()
+
+# use app as a global datastore
+app.comments = []
+
+
+@statefulCallbacks
+def getComments():
+    app = yield getApplication()
+    returnValue(app.comments)
+
+
+@app.route('/')
+@statefulCallbacks
+def index():
+    comments = yield getComments()
+    yield writeTemplate('guestbook.html', comments=app.comments)
+
+
+@app.route('/create')
+@statefulCallbacks
+def create_comment():
+    request = yield getRequest()
+
+    name = request.args['name'][0].decode('utf-8')
+    text = request.args['text'][0].decode('utf-8')
+
+    comments = yield getComments()
+    comments.append((name, text))
+
+    next_url = yield urlFor('index')
+    yield redirect(next_url)
+
+
+app.run(8080)

File examples/hello.py

+from tsumuji import Application, Jinja2TemplateMixin, \
+    statefulCallbacks, writeTemplate
+
+
+class HelloApplication(Application, Jinja2TemplateMixin):
+    pass
+
+
+app = HelloApplication()
+
+
+@app.route('/<name:str>')
+@app.route('/')
+@statefulCallbacks
+def hello(name='World!'):
+    yield writeTemplate('hello.html', name=name)
+
+
+app.run(8080)

File examples/templates/guestbook.html

+<!doctype html>
+<html>
+<head><title>Guest Book</title></head>
+<body>
+<h1>Guest Book</h1>
+<dl>
+{% for name, text in comments %}
+<dt>{{ name }}</dt>
+<dd>{{ text }}</dd>
+{% endfor %}
+</dl>
+<form action="{{ urlFor('create_comment') }}" method="POST">
+    name: <input type="text" name="name"><br />
+    comment: <input type="text" name="text"><br />
+    <input type="submit" value="submit">
+</form>
+</body>
+</html>

File examples/templates/hello.html

+<!doctype html>
+<html>
+<head><title>Hello {{ name }}</title></head>
+<body>
+<h1>Hello {{ name }}</h1>
+<p>Nice to meet you!</p>
+</body>
+</html>
+from distutils.core import setup
+
+
+setup(
+    name='tsumuji',
+    version='0.0.1',
+    packages=['tsumuji'],
+    install_requires=[
+        'Twisted',
+    ],
+    extras_require = {
+        'jinja2': ['Jinja2'],
+    },
+)

File tests/__init__.py

Empty file added.

File tests/templates/test.txt

+!{{ key }}!

File tests/test_application.py

+from twisted.trial.unittest import TestCase, SkipTest
+from twisted.web.test.test_web import DummyRequest
+
+
+class Jinja2TemplateMixinTestCase(TestCase):
+    def test_it(self):
+        try:
+            import imp
+            imp.find_module('jinja2')
+
+        except ImportError:
+            raise SkipTest('this test require jinja2 module installed')
+
+        from tsumuji.application import Application, Jinja2TemplateMixin, \
+            writeTemplate
+        from tsumuji.resources import statefulCallbacks
+
+        root = Application()
+
+        self.assertIsInstance(root, Jinja2TemplateMixin)
+
+        @root.route('/')
+        @statefulCallbacks
+        def handler():
+            yield writeTemplate('test.txt', key='value')
+
+        req = DummyRequest([''])
+        r = root.getChildWithDefault('', req)
+        r.render(req)
+
+        self.assertTrue(req.finished)
+        self.assertEqual(req.written, ['!value!'])

File tests/test_resources.py

+from twisted.trial.unittest import TestCase
+from twisted.web.test.test_web import DummyRequest
+
+
+class RootDispatchResourceTestCase(TestCase):
+    def test_it(self):
+        from twisted.web.resource import Resource
+        from tsumuji.resources import RootDispatchResource
+
+        root = RootDispatchResource()
+
+        @root.route('/archive/<year:int>/', name='year')
+        def yearHandler(year):
+            return year
+
+        @root.route('/archive/<year:int>/<month:int>/')
+        def yearMonthHandler(year, month):
+            return (year, month)
+
+        tr = Resource()
+        root.route('/twisted/resource')(tr)
+
+        r = root.getChildWithDefault('archive', DummyRequest([''])) \
+                .getChildWithDefault('2013', None) \
+                .getChildWithDefault('', None)
+
+        self.assertEqual(r._kwargs, {'year': 2013})
+        self.assertEqual(r.render(None), 2013)
+
+        r = root.getChildWithDefault('archive', DummyRequest([''])) \
+                .getChildWithDefault('2013', None) \
+                .getChildWithDefault('11', None) \
+                .getChildWithDefault('', None)
+
+        self.assertEqual(r._kwargs, {'year': 2013, 'month': 11})
+        self.assertEqual(r.render(None), (2013, 11))
+
+        r = root.getChildWithDefault('twisted', DummyRequest([''])) \
+                .getChildWithDefault('resource', None)
+
+        self.assertIs(r, tr)
+
+        url = root.urlFor('yearMonthHandler', year=2013, month=12)
+        self.assertEqual(url, '/archive/2013/12/')
+
+        url = root.urlFor('year', year=2013)
+        self.assertEqual(url, '/archive/2013/')
+
+    def test_404(self):
+        from tsumuji.resources import RootDispatchResource
+
+        root = RootDispatchResource()
+
+        @root.route('/archive/<year:int>/', name='year')
+        def yearHandler(year):
+            return year
+
+        @root.route('/archive/<year:int>/<month:int>/')
+        def yearMonthHandler(year, month):
+            return (year, month)
+
+        req = DummyRequest([''])
+        r = root.getChildWithDefault('unknown', req)
+        r.render(req)
+        self.assertEqual(req.responseCode, 404)
+
+        req = DummyRequest([''])
+        r = root.getChildWithDefault('archive', req)
+        r.render(req)
+        self.assertEqual(req.responseCode, 404)
+
+        req = DummyRequest([''])
+        r = root.getChildWithDefault('archive', req) \
+                .getChildWithDefault('2013', req) \
+                .getChildWithDefault('11', req) \
+                .getChildWithDefault('13', req)
+        r.render(req)
+        self.assertEqual(req.responseCode, 404)
+
+    def test_state(self):
+        from twisted.web import server
+        from tsumuji.resources import RootDispatchResource, \
+            statefulCallbacks, getRoot, getRequest, returnValue
+
+        root = RootDispatchResource()
+
+        results = {}
+
+        @root.route('/')
+        @statefulCallbacks
+        def handler():
+            results['root'] = yield getRoot()
+            results['request'] = yield getRequest()
+
+            returnValue('end')
+
+        req = DummyRequest([''])
+        r = root.getChildWithDefault('', req)
+        ret = r.render(req)
+        self.assertEqual(ret, server.NOT_DONE_YET)
+
+        self.assertTrue(req.finished)
+        self.assertIs(results['root'], root)
+        self.assertIs(results['request'], req)
+
+
+class MatcherTestCase(TestCase):
+    def test_StaticMatcher(self):
+        from tsumuji.resources import StaticMatcher, NotMatch
+
+        m = StaticMatcher('some_string')
+
+        self.assertEqual(m.match('some_string'), (None, 'some_string'))
+        self.assertRaises(NotMatch, m.match, 'other_string')
+
+    def test_StringMatcher(self):
+        from tsumuji.resources import StringMatcher
+
+        m = StringMatcher('myName')
+
+        self.assertEqual(m.match('some_string'), ('myName', 'some_string'))
+        self.assertEqual(m.match('other_string'), ('myName', 'other_string'))
+
+    def test_IntMatcher(self):
+        from tsumuji.resources import IntMatcher, NotMatch
+
+        m = IntMatcher('myName')
+
+        self.assertEqual(m.match('42'), ('myName', 42))
+        self.assertRaises(NotMatch, m.match, 'some_string')
+
+
+class HandlerTestCase(TestCase):
+    def test_handler(self):
+        from tsumuji.resources import Handler
+
+        def handler(arg1, arg2):
+            return arg1 + arg2
+
+        h = Handler(None, handler)
+        ret = h.render(None, {'arg1': 'value1', 'arg2': 'value2'})
+
+        self.assertEqual(ret, 'value1value2')
+
+    def test_stateful(self):
+        from twisted.web import server
+        from tsumuji.resources import Handler, statefulCallbacks, returnValue
+
+        @statefulCallbacks
+        def handler(arg1, arg2):
+            returnValue(arg1 + arg2)
+
+        req = DummyRequest([''])
+        h = Handler(None, handler)
+        ret = h.render(req, {'arg1': 'value1', 'arg2': 'value2'})
+
+        self.assertEqual(ret, server.NOT_DONE_YET)
+        self.assertTrue(req.finished)
+        self.assertEqual(req.written, ['value1value2'])
+
+
+class HelperFunctionsTestCase(TestCase):
+    def test_setHeader(self):
+        from tsumuji.resources import RequestState, setHeader
+
+        req = DummyRequest([''])
+        state = RequestState(request=req)
+        d = setHeader('x-my-header', 'my header').run(state)
+
+        def cb((result, state)):
+            self.assertEqual(req.outgoingHeaders, {'x-my-header': 'my header'})
+
+        d.addCallback(cb)
+        return d
+
+    def test_setCode(self):
+        from tsumuji.resources import RequestState, setCode
+
+        req = DummyRequest([''])
+        state = RequestState(request=req)
+        d = setCode(304).run(state)
+
+        def cb((result, state)):
+            self.assertEqual(req.responseCode, 304)
+
+        d.addCallback(cb)
+        return d
+
+    def test_writeBody(self):
+        from tsumuji.resources import RequestState, \
+            statefulCallbacks, writeBody
+
+        @statefulCallbacks
+        def handler():
+            yield writeBody('hello world\n')
+            yield writeBody('hello twisted\n')
+
+        req = DummyRequest([''])
+        state = RequestState(request=req)
+        d = handler().run(state)
+
+        def cb((result, state)):
+            self.assertEqual(req.written, ['hello world\n', 'hello twisted\n'])
+
+        d.addCallback(cb)
+        return d
+
+    def test_writeBody_unicode(self):
+        from tsumuji.resources import RequestState, \
+            statefulCallbacks, writeBody
+
+        @statefulCallbacks
+        def handler():
+            yield writeBody('hello world\n')
+
+        req = DummyRequest([''])
+        state = RequestState(request=req)
+        d = handler().run(state)
+
+        def cb((result, state)):
+            self.assertIsInstance(req.written[0], str)
+
+        d.addCallback(cb)
+        return d
+
+    def test_redirect(self):
+        from tsumuji.resources import RequestState, redirect
+
+        req = DummyRequest([''])
+        state = RequestState(request=req)
+        d = redirect('/egg').run(state)
+
+        def cb((result, state)):
+            self.assertEqual(req.responseCode, 302)
+            self.assertEqual(req.outgoingHeaders, {'location': '/egg'})
+
+        d = d.addCallback(cb)
+        return d

File tests/test_statefulcallbacks.py

+from twisted.internet import defer
+from twisted.trial.unittest import TestCase
+
+
+class StatefulCallbacksTestCase(TestCase):
+    def test_StatefulCallbacks(self):
+        from tsumuji.statefulcallbacks import StatefulCallbacks, \
+            getState, setState, returnValue
+
+        def test():
+            st = yield getState()
+            yield setState(10)
+            returnValue(st + 1)
+
+        s = StatefulCallbacks(test, (), {})
+        d = s.run(1)
+        d.addCallback(lambda r: self.assertEqual(r, (2, 10)))
+
+        return d
+
+    def test_statefulCallbacks(self):
+        from tsumuji.statefulcallbacks import statefulCallbacks, \
+            getState, setState, returnValue
+
+        @statefulCallbacks
+        def test():
+            st = yield getState()
+            yield setState(10)
+            returnValue(st + 1)
+
+        d = test().run(1)
+        d.addCallback(lambda r: self.assertEqual(r, (2, 10)))
+
+        return d
+
+    def test_sleepAndGetState(self):
+        from tsumuji.statefulcallbacks import statefulCallbacks, \
+            getState, setState, returnValue
+
+        def sleep(sec):
+            from twisted.internet import reactor
+
+            d = defer.Deferred()
+            reactor.callLater(sec, d.callback, None)
+
+            return d
+
+        @statefulCallbacks
+        def sleepAndGetState():
+            yield sleep(0.1)
+            st = yield getState()
+            yield setState(10)
+            returnValue(st + 1)
+
+        d = sleepAndGetState().run(1)
+        d.addCallback(lambda r: self.assertEqual(r, (2, 10)))
+
+        return d
+
+    def test_returnEarly(self):
+        from tsumuji.statefulcallbacks import statefulCallbacks, returnValue
+
+        @statefulCallbacks
+        def returnEarly():
+            returnValue(1)
+
+        d = returnEarly().run(None)
+        d.addCallback(lambda r: self.assertEqual(r, (1, None)))
+
+        return d

File tsumuji/__init__.py

+from .application import Application, Jinja2TemplateMixin, getApplication, \
+    renderTemplate, writeTemplate
+from .statefulcallbacks import statefulCallbacks, returnValue
+from .resources import getRequest, setHeader, setCode, urlFor, redirect

File tsumuji/application.py

+from twisted.web import server
+
+from .resources import RootDispatchResource, statefulCallbacks, \
+    getRoot, getState, returnValue, writeBody
+
+
+class BaseApplication(RootDispatchResource):
+    def run(self, port=8080):
+        import sys
+
+        from twisted.internet import reactor
+        from twisted.python import log
+
+        log.startLogging(sys.stderr)
+        reactor.listenTCP(8080, self.site())
+        reactor.run()
+
+    def site(self):
+        return server.Site(self)
+
+
+class Jinja2TemplateMixin(object):
+    templateDirName = 'templates'
+
+    def renderTemplate(self, state, templateName, **context):
+        if not hasattr(self, '_jinja_environments'):
+            self._jinjaEnvironments = {}
+
+        templateDir = self._getTemplateDir(state._handler)
+
+        env = self._jinjaEnvironments.get(templateDir)
+
+        if env is None:
+            from jinja2 import Environment, FileSystemLoader
+            from jinja2.ext import loopcontrols, with_
+
+            env = Environment(loader=FileSystemLoader([templateDir]),
+                              extensions=[loopcontrols, with_])
+
+            self._jinjaEnvironments[templateDir] = env
+
+        template_context = {
+            'application': self,
+            'urlFor': self.urlFor,
+            'request': state.request,
+        }
+        template_context.update(context)
+
+        return env.get_template(templateName).render(**template_context)
+
+    def _getTemplateDir(self, handler):
+        import os
+        import sys
+
+        module_path = getattr(sys.modules[handler.__module__], '__file__', '')
+
+        module_dir = os.path.dirname(module_path)
+
+        return os.path.join(module_dir, self.templateDirName)
+
+
+try:
+    import jinja2
+
+    class Application(BaseApplication, Jinja2TemplateMixin):
+        pass
+
+except ImportError:
+    class Application(BaseApplication):
+        pass
+
+
+getApplication = getRoot
+
+
+@statefulCallbacks
+def renderTemplate(templateName, **context):
+    st = yield getState()
+    returnValue(st.root.renderTemplate(st, templateName, **context))
+
+
+@statefulCallbacks
+def writeTemplate(templateName, **context):
+    text = yield renderTemplate(templateName, **context)
+    yield writeBody(text)

File tsumuji/resources.py

+# vim: fileencoding=utf-8
+import re
+
+from collections import OrderedDict
+
+from twisted.python.compat import intToBytes
+from twisted.web import resource, server
+from zope.interface import Interface, implements
+
+from .statefulcallbacks import StatefulCallbacks, statefulCallbacks, \
+    returnValue, getState
+
+
+class RootDispatchResource(object):
+    '''IResource provider with URL dispatch support
+    '''
+    implements(resource.IResource)
+
+    ## IResource requirements
+    isLeaf = False
+
+    def getChildWithDefault(self, name, request):
+        if self._urlPrefix is None:
+            self._urlPrefix = '/'.join([''] + request.prepath[:-1]) + '/'
+
+        dispatching = DispatchingResource(None, self._children, {})
+        return dispatching.getChildWithDefault(name, request)
+
+    def putChild(self, path, child):
+        self.route(path, child)
+
+    def render(self, request):
+        raise NotImplementedError()
+
+    ## end IResource requirements
+
+    def __init__(self):
+        self._children = OrderedDict()
+        self._nameToPattern = {}
+        self._urlPrefix = None
+
+    def route(self, pattern, name=None):
+        '''add request handler or resource to route
+
+        To add function `hello` to route `/hello`:
+
+        >>> root = RootDispatchResource()
+        >>> @root.route('/hello')
+        ... def hello():
+        ...     return 'hello world!'
+
+        Also, you can add resources to route:
+
+        >>> from twisted.web import static
+        >>> root.route('/file')(static.File('.'))
+
+        '''
+        def decorator(handlerOrResource):
+            hname = None
+
+            if name is not None:
+                hname = name
+
+            else:
+                if hasattr(handlerOrResource, '__name__'):
+                    hname = handlerOrResource.__name__
+
+                elif hasattr(type(handlerOrResource), '__name__'):
+                    hname = type(handlerOrResource).__name__
+
+            self._route(pattern, handlerOrResource, hname)
+
+            return handlerOrResource
+
+        return decorator
+
+    def _route(self, pattern, handlerOrResource, name=None):
+        if name is not None:
+            self._nameToPattern[name] = pattern
+
+        if pattern.startswith('/'):
+            pattern = pattern.lstrip('/')
+
+        paths = pattern.split('/')
+        children = self._children
+
+        for path in paths:
+            if path in children:
+                child = children[path]
+
+            else:
+                mobj = re.match(r'<(?:(?P<name>\w+):)?(?P<type>\w+)>', path)
+                if mobj:
+                    name = mobj.groupdict().get('name')
+                    mtype = mobj.groupdict().get('type')
+
+                    if mtype == 'str':
+                        matcher = StringMatcher(name)
+
+                    elif mtype == 'int':
+                        matcher = IntMatcher(name)
+
+                    else:
+                        raise Exception(
+                            'unknown matcher type: {0}'.format(mtype))
+
+                else:
+                    matcher = StaticMatcher(path)
+
+                child = DispatchFactory(matcher)
+                children[path] = child
+
+            children = child.children
+
+        if resource.IResource.providedBy(handlerOrResource):
+            child.resource = handlerOrResource
+        else:
+            child.handler = Handler(self, handlerOrResource)
+
+    def urlFor(self, name, **kwargs):
+        '''Get URL for `name` handler
+        '''
+        assert(self._urlPrefix)
+
+        pattern = self._nameToPattern[name]
+        url = re.sub('<(\w+):\w+>',
+                     lambda m: str(kwargs[m.group(1)]),
+                     pattern)
+
+        url = self._urlPrefix + url.lstrip('/')
+
+        return url
+
+
+class Matcher(Interface):
+    def match(string):
+        """Check to match string with this matcher
+
+        if string match, return name and value pair (name may None).
+        if not, raise NotMatch exception.
+        """
+
+
+class NotMatch(BaseException):
+    pass
+
+
+class StaticMatcher(object):
+    implements(Matcher)
+
+    def __init__(self, string, name=None):
+        self._name = name
+        self._string = string
+
+    def match(self, string):
+        if string == self._string:
+            return (self._name, string)
+
+        else:
+            raise NotMatch()
+
+
+class StringMatcher(object):
+    implements(Matcher)
+
+    def __init__(self, name=None):
+        self._name = name
+
+    def match(self, string):
+        return (self._name, string)
+
+
+class IntMatcher(object):
+    implements(Matcher)
+
+    def __init__(self, name=None):
+        self._name = name
+
+    def match(self, string):
+        if string.isdigit():
+            return (self._name, int(string))
+
+        else:
+            raise NotMatch()
+
+
+class DispatchFactory(object):
+    def __init__(self, matcher):
+        self.matcher = matcher
+
+        self.resource = None
+        self.handler = None
+
+        self.children = OrderedDict()
+
+    def getResource(self, kwargs):
+        if self.resource is not None:
+            return self.resource
+
+        else:
+            return DispatchingResource(self.handler, self.children, kwargs)
+
+
+class DispatchingResource(object):
+    implements(resource.IResource)
+
+    ## IResource requirements
+    isLeaf = False
+
+    def getChildWithDefault(self, name, request):
+        for child in self._children.values():
+            try:
+                name, value = child.matcher.match(name)
+
+            except NotMatch:
+                continue
+
+            kwargs = self._kwargs
+            if name:
+                kwargs[name] = value
+
+            return child.getResource(kwargs)
+
+        else:
+            return resource.NoResource()
+
+    def putChild(self, path, child):
+        raise NotImplementedError()
+
+    def render(self, request):
+        if self.handler is not None:
+            return self.handler.render(request, self._kwargs)
+
+        else:
+            # 404 Not Found
+            return resource.NoResource().render(request)
+
+    ## end IResource requirements
+
+    def __init__(self, handler, children, kwargs):
+        self.handler = handler
+        self._children = children
+        self._kwargs = kwargs
+
+
+class Handler(object):
+    '''Request handler function wrapper
+    '''
+    def __init__(self, root, handler):
+        self.root = root
+        self._handler = handler
+
+    def render(self, request, kwargs):
+        state = RequestState(
+            request=request,
+            root=self.root,
+
+            _handler=self._handler
+        )
+
+        r = self._handler(**kwargs)
+
+        if isinstance(r, StatefulCallbacks):
+            r.eval(state).addCallback(
+                self._renderCallback, state
+            ).addErrback(request.processingFailed)
+
+            return server.NOT_DONE_YET
+        else:
+            return r
+
+    def _renderCallback(self, result, state):
+        if result is not None:
+            state.request.setHeader(b'content-length', intToBytes(len(result)))
+            state.request.write(result)
+
+        state.request.finish()
+
+
+class RequestState(object):
+    def __init__(self, **kwargs):
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+
+# helper functions
+@statefulCallbacks
+def getRequest():
+    returnValue((yield getState()).request)
+
+
+@statefulCallbacks
+def getRoot():
+    returnValue((yield getState()).root)
+
+
+@statefulCallbacks
+def setHeader(name, value):
+    (yield getRequest()).setHeader(name, value)
+
+
+@statefulCallbacks
+def setCode(code, message=None):
+    (yield getRequest()).setResponseCode(code, message)
+
+
+@statefulCallbacks
+def writeBody(body):
+    if isinstance(body, unicode):
+        body = body.encode('utf-8')
+
+    (yield getRequest()).write(body)
+
+
+@statefulCallbacks
+def urlFor(name, **kwargs):
+    root = yield getRoot()
+    returnValue(root.urlFor(name, **kwargs))
+
+
+@statefulCallbacks
+def redirect(url, code=302, message='Found'):
+    yield setCode(code, message)
+    yield setHeader('location', url)

File tsumuji/statefulcallbacks.py

+from __future__ import print_function
+
+import functools
+import types
+
+from twisted.internet import defer
+
+
+__all__ = ['StatefulCallbacks', 'statefulCallbacks',
+           'returnValue', 'getState', 'setState']
+
+
+class StatefulCallbacks(object):
+    def __init__(self, gen, args, kwargs):
+        self._gen = gen
+        self._args = args
+        self._kwargs = kwargs
+
+    def eval(self, state):
+        return self.run(state).addCallback(lambda (v, s): v)
+
+    @defer.inlineCallbacks
+    def run(self, state):
+        try:
+            g = self._gen(*self._args, **self._kwargs)
+
+        except defer._DefGen_Return as e:
+            defer.returnValue((e.value, state))
+
+        if not isinstance(g, types.GeneratorType):
+            raise TypeError(
+                '{0} returns non generator object {1}'.format(self._gen, g))
+
+        result = next(g)
+
+        while True:
+            if isinstance(result, StatefulCallbacks):
+                (next_input, state) = yield result.run(state)
+
+            elif isinstance(result, _GetState):
+                next_input = state
+
+            elif isinstance(result, _SetState):
+                next_input = None
+                state = result.state
+
+            elif isinstance(result, defer.Deferred):
+                next_input = yield result
+
+            else:
+                next_input = result
+
+            try:
+                result = g.send(next_input)
+
+            except StopIteration:
+                defer.returnValue((None, state))
+
+            except defer._DefGen_Return as e:
+                defer.returnValue((e.value, state))
+
+
+def statefulCallbacks(func):
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        return StatefulCallbacks(func, args, kwargs)
+
+    return wrapper
+
+
+class _GetState(object):
+    pass
+
+
+getState = _GetState
+
+
+class _SetState(object):
+    def __init__(self, state):
+        self.state = state
+
+
+setState = _SetState
+
+returnValue = defer.returnValue