Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added class based views

  • Loading branch information...
commit dcf21989dc71f4ab93ceaf0627e40c516462bd8c 1 parent 1c8097b
@mitsuhiko authored
Showing with 147 additions and 3 deletions.
  1. +8 −3 flask/app.py
  2. +102 −0 flask/views.py
  3. +37 −0 tests/flask_tests.py
View
11 flask/app.py
@@ -642,7 +642,8 @@ def register_blueprint(self, blueprint, **options):
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 @@ def index():
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 @@ def decorator(f):
return f
return decorator
-
def endpoint(self, endpoint):
"""A decorator to register a function as an endpoint.
Example::
View
102 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/<name>', 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)
View
37 tests/flask_tests.py
@@ -14,6 +14,7 @@
import re
import sys
import flask
+import flask.views
import unittest
import warnings
from threading import Thread
@@ -23,6 +24,7 @@
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 @@ def record(sender, exception):
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))
Please sign in to comment.
Something went wrong with that request. Please try again.