From dcf21989dc71f4ab93ceaf0627e40c516462bd8c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 12:45:49 +0200 Subject: [PATCH] Added class based views --- flask/app.py | 11 +++-- flask/views.py | 102 +++++++++++++++++++++++++++++++++++++++++++ tests/flask_tests.py | 37 ++++++++++++++++ 3 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 flask/views.py diff --git a/flask/app.py b/flask/app.py index 03a0c9f1..eb7ba023 100644 --- a/flask/app.py +++ b/flask/app.py @@ -642,7 +642,8 @@ class Flask(_PackageBoundObject): if blueprint.name in self.blueprints: assert self.blueprints[blueprint.name] is blueprint, \ 'A blueprint\'s name collision ocurred between %r and ' \ - '%r. Both share the same name "%s"' % \ + '%r. Both share the same name "%s". Blueprints that ' \ + 'are created on the fly need unique names.' % \ (blueprint, self.blueprints[blueprint.name], blueprint.name) else: self.blueprints[blueprint.name] = blueprint @@ -695,7 +696,12 @@ class Flask(_PackageBoundObject): if endpoint is None: endpoint = _endpoint_from_view_func(view_func) options['endpoint'] = endpoint - methods = options.pop('methods', ('GET',)) + methods = options.pop('methods', None) + # if the methods are not given and the view_func object knows its + # methods we can use that instead. If neither exists, we go with + # a tuple of only `GET` as default. + if methods is None: + methods = getattr(view_func, 'methods', None) or ('GET',) provide_automatic_options = False if 'OPTIONS' not in methods: methods = tuple(methods) + ('OPTIONS',) @@ -778,7 +784,6 @@ class Flask(_PackageBoundObject): return f return decorator - def endpoint(self, endpoint): """A decorator to register a function as an endpoint. Example:: diff --git a/flask/views.py b/flask/views.py new file mode 100644 index 00000000..71f428f3 --- /dev/null +++ b/flask/views.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +""" + flask.views + ~~~~~~~~~~~ + + This module provides class based views inspired by the ones in Django. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from .globals import request + + +http_method_funcs = frozenset(['get', 'post', 'head', 'options', + 'delete', 'put', 'trace']) + + + +class View(object): + """Alternative way to use view functions. A subclass has to implement + :meth:`dispatch_request` which is called with the view arguments from + the URL routing system. If :attr:`methods` is provided the methods + do not have to be passed to the :meth:`~flask.Flask.add_url_rule` + method explicitly:: + + class MyView(View): + methods = ['GET'] + + def dispatch_request(self, name): + return 'Hello %s!' % name + + app.add_url_rule('/hello/', view_func=MyView.as_view('myview')) + """ + + methods = None + + def dispatch_request(self): + raise NotImplementedError() + + @classmethod + def as_view(cls, name, *class_args, **class_kwargs): + """Converts the class into an actual view function that can be + used with the routing system. What it does internally is generating + a function on the fly that will instanciate the :class:`View` + on each request and call the :meth:`dispatch_request` method on it. + + The arguments passed to :meth:`as_view` are forwarded to the + constructor of the class. + """ + def view(*args, **kwargs): + self = cls(*class_args, **class_kwargs) + return self.dispatch_request(*args, **kwargs) + view.__name__ = name + view.__doc__ = cls.__doc__ + view.__module__ = cls.__module__ + view.methods = cls.methods + return view + + +class MethodViewType(type): + + def __new__(cls, name, bases, d): + rv = type.__new__(cls, name, bases, d) + if rv.methods is None: + methods = [] + for key, value in d.iteritems(): + if key in http_method_funcs: + methods.append(key.upper()) + # if we have no method at all in there we don't want to + # add a method list. (This is for instance the case for + # the baseclass or another subclass of a base method view + # that does not introduce new methods). + if methods: + rv.methods = methods + return rv + + +class MethodView(View): + """Like a regular class based view but that dispatches requests to + particular methods. For instance if you implement a method called + :meth:`get` it means you will response to ``'GET'`` requests and + the :meth:`dispatch_request` implementation will automatically + forward your request to that. Also :attr:`options` is set for you + automatically:: + + class CounterAPI(MethodView): + + def get(self): + return session.get('counter', 0) + + def post(self): + session['counter'] = session.get('counter', 0) + 1 + return 'OK' + + app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) + """ + __metaclass__ = MethodViewType + + def dispatch_request(self, *args, **kwargs): + meth = getattr(self, request.method.lower(), None) + assert meth is not None, 'Not implemented method' + return meth(*args, **kwargs) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index ae43f93f..80ccaffb 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -14,6 +14,7 @@ import os import re import sys import flask +import flask.views import unittest import warnings from threading import Thread @@ -23,6 +24,7 @@ from functools import update_wrapper from datetime import datetime from werkzeug import parse_date, parse_options_header from werkzeug.exceptions import NotFound +from werkzeug.http import parse_set_header from jinja2 import TemplateNotFound from cStringIO import StringIO @@ -1753,6 +1755,40 @@ class TestSignals(unittest.TestCase): flask.got_request_exception.disconnect(record, app) +class ViewTestCase(unittest.TestCase): + + def common_test(self, app): + c = app.test_client() + + self.assertEqual(c.get('/').data, 'GET') + self.assertEqual(c.post('/').data, 'POST') + self.assertEqual(c.put('/').status_code, 405) + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assertEqual(sorted(meths), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_basic_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.View): + methods = ['GET', 'POST'] + def dispatch_request(self): + return flask.request.method + + app.add_url_rule('/', view_func=Index.as_view('index')) + self.common_test(app) + + def test_method_based_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + app.add_url_rule('/', view_func=Index.as_view('index')) + self.common_test(app) + class DeprecationsTestCase(unittest.TestCase): def test_init_jinja_globals(self): @@ -1785,6 +1821,7 @@ def suite(): suite.addTest(unittest.makeSuite(LoggingTestCase)) suite.addTest(unittest.makeSuite(ConfigTestCase)) suite.addTest(unittest.makeSuite(SubdomainTestCase)) + suite.addTest(unittest.makeSuite(ViewTestCase)) suite.addTest(unittest.makeSuite(DeprecationsTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase))