diff --git a/README.rst b/README.rst index ee6fe7e66..e8dece7a4 100644 --- a/README.rst +++ b/README.rst @@ -187,3 +187,50 @@ must be defined before the handler with no ``methods``. The following curl command can be used to test this behaviour:: curl -v -L -d name='bob' http://localhost:8080/ + + +Non-global state +~~~~~~~~~~~~~~~~ + +For obvious reasons it may be desirable for your application to have some +non-global state that is used by the your route handlers. + +Below we have created a simple ``ItemStore`` class that has an instance of +``Klein`` as a class variable ``app``. We can now use ``@app.route`` to +decorate the methods of the class. + + +.. code-block:: python + + import json + + from klein import Klein + + + class ItemStore(object): + app = Klein() + + def __init__(self): + self._items = {} + + @app.route('/') + def items(self, request): + request.setHeader('Content-Type', 'application/json') + return json.dumps(self._items) + + @app.route('/', methods=['PUT']) + def save_item(self, request, name): + request.setHeader('Content-Type', 'application/json') + body = json.loads(request.content.read()) + self._items[name] = body + return json.dumps({'success': True}) + + @app.route('/', methods=['GET']) + def get_item(self, request, name): + request.setHeader('Content-Type', 'application/json') + return json.dumps(self._items.get(name)) + + + if __name__ == '__main__': + store = ItemStore() + store.app.run('localhost', 8080) diff --git a/klein/app.py b/klein/app.py index dc02fe306..d4be19f12 100644 --- a/klein/app.py +++ b/klein/app.py @@ -2,6 +2,7 @@ Applications are great. Lets have more of them. """ import sys +import weakref from functools import wraps @@ -45,9 +46,12 @@ class Klein(object): @ivar _endpoints: A C{dict} mapping endpoint names to handler functions. """ + _bound_klein_instances = weakref.WeakKeyDictionary() + def __init__(self): self._url_map = Map() self._endpoints = {} + self._instance = None @property @@ -66,6 +70,15 @@ def endpoints(self): return self._endpoints + def execute_endpoint(self, endpoint, *args, **kwargs): + """ + Execute the named endpoint with all arguments and possibly a bound + instance. + """ + endpoint_f = self._endpoints[endpoint] + return endpoint_f(self._instance, *args, **kwargs) + + def resource(self): """ Return an L{IResource} which suitably wraps this app. @@ -76,6 +89,25 @@ def resource(self): return KleinResource(self) + def __get__(self, instance, owner): + """ + Get an instance of L{Klein} bound to C{instance}. + """ + if instance is None: + return self + + k = self._bound_klein_instances.get(instance) + + if k is None: + k = self.__class__() + k._url_map = self._url_map + k._endpoints = self._endpoints + k._instance = instance + self._bound_klein_instances[instance] = k + + return k + + def route(self, url, *args, **kwargs): """ Add a new handler for C{url} passing C{args} and C{kwargs} directly to @@ -99,6 +131,12 @@ def index(request): @returns: decorated handler function. """ + def call(instance, f, *args, **kwargs): + if instance is None: + return f(*args, **kwargs) + + return f(instance, *args, **kwargs) + def deco(f): kwargs.setdefault('endpoint', f.__name__) if kwargs.pop('branch', False): @@ -107,14 +145,18 @@ def deco(f): branchKwargs['endpoint'] = branchKwargs['endpoint'] + '_branch' @wraps(f) - def branch_f(request, *a, **kw): + def branch_f(instance, request, *a, **kw): IKleinRequest(request).branch_segments = kw.pop('__rest__', '').split('/') - return f(request, *a, **kw) + return call(instance, f, request, *a, **kw) self._endpoints[branchKwargs['endpoint']] = branch_f self._url_map.add(Rule(url.rstrip('/') + '/' + '', *args, **branchKwargs)) - self._endpoints[kwargs['endpoint']] = f + @wraps(f) + def _f(instance, request, *a, **kw): + return call(instance, f, request, *a, **kw) + + self._endpoints[kwargs['endpoint']] = _f self._url_map.add(Rule(url, *args, **kwargs)) return f diff --git a/klein/resource.py b/klein/resource.py index 801c65a06..4f1d67d12 100644 --- a/klein/resource.py +++ b/klein/resource.py @@ -67,12 +67,13 @@ def render(self, request): return he.get_body({}) - handler = self._app.endpoints[endpoint] - # Standard Twisted Web stuff. Defer the method action, giving us # something renderable or printable. Return NOT_DONE_YET and set up # the incremental renderer. - d = defer.maybeDeferred(handler, request, **kwargs) + d = defer.maybeDeferred(self._app.execute_endpoint, + endpoint, + request, + **kwargs) def process(r): if IResource.providedBy(r): diff --git a/klein/test_app.py b/klein/test_app.py index 43bba9356..b3cb394eb 100644 --- a/klein/test_app.py +++ b/klein/test_app.py @@ -4,7 +4,25 @@ from mock import Mock, patch +from twisted.python.components import registerAdapter + from klein import Klein +from klein.app import KleinRequest +from klein.interfaces import IKleinRequest + + +class DummyRequest(object): + def __init__(self, n): + self.n = n + + def __eq__(self, other): + return other.n == self.n + + def __repr__(self): + return ''.format(n=self.n) + + +registerAdapter(KleinRequest, DummyRequest, IKleinRequest) class KleinTestCase(unittest.TestCase): @@ -21,9 +39,8 @@ def foo(request): c = app.url_map.bind("foo") self.assertEqual(c.match("/foo"), ("foo", {})) self.assertEqual(len(app.endpoints), 1) - self.assertIdentical(app.endpoints["foo"], foo) - self.assertEqual(app.endpoints["foo"](None), "foo") + self.assertEqual(app.execute_endpoint("foo", DummyRequest(1)), "foo") def test_stackedRoute(self): @@ -42,12 +59,10 @@ def foobar(request): c = app.url_map.bind("foo") self.assertEqual(c.match("/foo"), ("foobar", {})) - self.assertIdentical(app.endpoints["foobar"], foobar) + self.assertEqual(app.execute_endpoint("foobar", DummyRequest(1)), "foobar") self.assertEqual(c.match("/bar"), ("bar", {})) - self.assertIdentical(app.endpoints["foobar"], foobar) - - self.assertEqual(app.endpoints["foobar"](None), "foobar") + self.assertEqual(app.execute_endpoint("bar", DummyRequest(2)), "foobar") def test_branchRoute(self): @@ -67,11 +82,96 @@ def foo(request): c.match("/foo/bar"), ("foo_branch", {'__rest__': 'bar'})) - self.assertIdentical(app.endpoints["foo"], foo) - self.assertNotIdentical(app.endpoints["foo_branch"], foo) + self.assertEquals(app.endpoints["foo"].__name__, "foo") self.assertEquals( - app.endpoints["foo"].__name__, - app.endpoints["foo"].__name__) + app.endpoints["foo_branch"].__name__, + "foo") + + + def test_classicalRoute(self): + """ + L{Klein.route} may be used a method decorator when a L{Klein} instance + is defined as a class variable. + """ + bar_calls = [] + class Foo(object): + app = Klein() + + @app.route("/bar") + def bar(self, request): + bar_calls.append((self, request)) + return "bar" + + foo = Foo() + c = foo.app.url_map.bind("bar") + self.assertEqual(c.match("/bar"), ("bar", {})) + self.assertEquals(foo.app.execute_endpoint("bar", DummyRequest(1)), "bar") + + self.assertEqual(bar_calls, [(foo, DummyRequest(1))]) + + + def test_classicalRouteWithTwoInstances(self): + """ + Multiple instances of a class with a L{Klein} attribute and + L{Klein.route}'d methods can be created and their L{Klein}s used + independently. + """ + class Foo(object): + app = Klein() + + def __init__(self): + self.bar_calls = [] + + @app.route("/bar") + def bar(self, request): + self.bar_calls.append((self, request)) + return "bar" + + foo_1 = Foo() + foo_1_app = foo_1.app + foo_2 = Foo() + foo_2_app = foo_2.app + + dr1 = DummyRequest(1) + dr2 = DummyRequest(2) + + foo_1_app.execute_endpoint('bar', dr1) + foo_2_app.execute_endpoint('bar', dr2) + self.assertEqual(foo_1.bar_calls, [(foo_1, dr1)]) + self.assertEqual(foo_2.bar_calls, [(foo_2, dr2)]) + + + def test_classicalRouteWithBranch(self): + """ + Multiple instances of a class with a L{Klein} attribute and + L{Klein.route}'d methods can be created and their L{Klein}s used + independently. + """ + class Foo(object): + app = Klein() + + def __init__(self): + self.bar_calls = [] + + @app.route("/bar/", branch=True) + def bar(self, request): + self.bar_calls.append((self, request)) + return "bar" + + foo_1 = Foo() + foo_1_app = foo_1.app + foo_2 = Foo() + foo_2_app = foo_2.app + + dr1 = DummyRequest(1) + dr2 = DummyRequest(2) + + foo_1_app.execute_endpoint('bar_branch', dr1) + foo_2_app.execute_endpoint('bar_branch', dr2) + self.assertEqual(foo_1.bar_calls, [(foo_1, dr1)]) + self.assertEqual(foo_2.bar_calls, [(foo_2, dr2)]) + + def test_branchDoesntRequireTrailingSlash(self):