From 5e1b1030e8edecb0c9652f2a406933c049ea2397 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Jul 2010 23:04:24 +0200 Subject: [PATCH] Added support for automagic OPTIONS --- CHANGES | 3 +++ docs/quickstart.rst | 8 +++++++- flask/app.py | 37 +++++++++++++++++++++++++++++++------ flask/ctx.py | 5 +++-- flask/helpers.py | 1 + flask/wrappers.py | 21 ++++++++++++++++----- tests/flask_tests.py | 17 +++++++++++++---- 7 files changed, 74 insertions(+), 18 deletions(-) diff --git a/CHANGES b/CHANGES index c09b9e04..0c8f965e 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,9 @@ Release date to be announced, codename to be decided. - after request functions are now called in reverse order of registration. +- OPTIONS is now automatically implemented by Flask unless the + application explictly adds 'OPTIONS' as method to the URL rule. + In this case no automatic OPTIONS handling kicks in. Version 0.5.1 ------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e1fdce51..4e98f858 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -269,7 +269,8 @@ If `GET` is present, `HEAD` will be added automatically for you. You don't have to deal with that. It will also make sure that `HEAD` requests are handled like the `HTTP RFC`_ (the document describing the HTTP protocol) demands, so you can completely ignore that part of the HTTP -specification. +specification. Likewise as of Flask 0.6, `OPTIONS` is implemented for you +as well automatically. You have no idea what an HTTP method is? Worry not, here quick introduction in HTTP methods and why they matter: @@ -310,6 +311,11 @@ very common: `DELETE` Remove the information that the given location. +`OPTIONS` + Provides a quick way for a requesting client to figure out which + methods are supported by this URL. Starting with Flask 0.6, this + is implemented for you automatically. + Now the interesting part is that in HTML4 and XHTML1, the only methods a form might submit to the server are `GET` and `POST`. But with JavaScript and future HTML standards you can use other methods as well. Furthermore diff --git a/flask/app.py b/flask/app.py index 49852e5e..02f22aa3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -464,6 +464,9 @@ class Flask(_PackageBoundObject): .. versionchanged:: 0.2 `view_func` parameter added. + .. versionchanged:: 0.6 + `OPTIONS` is added automatically as method. + :param rule: the URL rule as string :param endpoint: the endpoint for the registered URL rule. Flask itself assumes the name of the view function as @@ -471,15 +474,27 @@ class Flask(_PackageBoundObject): :param view_func: the function to call when serving a request to the provided endpoint :param options: the options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object + :class:`~werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods + is a list of methods this rule should be limited + to (`GET`, `POST` etc.). By default a rule + just listens for `GET` (and implicitly `HEAD`). + Starting with Flask 0.6, `OPTIONS` is implicitly + added and handled by the standard request handling. """ if endpoint is None: assert view_func is not None, 'expected view func if endpoint ' \ 'is not provided.' endpoint = view_func.__name__ options['endpoint'] = endpoint - options.setdefault('methods', ('GET',)) - self.url_map.add(Rule(rule, **options)) + methods = options.pop('methods', ('GET',)) + provide_automatic_options = False + if 'OPTIONS' not in methods: + methods = tuple(methods) + ('OPTIONS',) + provide_automatic_options = True + rule = Rule(rule, methods=methods, **options) + rule.provide_automatic_options = provide_automatic_options + self.url_map.add(rule) if view_func is not None: self.view_functions[endpoint] = view_func @@ -539,8 +554,10 @@ class Flask(_PackageBoundObject): :param rule: the URL rule as string :param methods: a list of methods this rule should be limited - to (``GET``, ``POST`` etc.). By default a rule - just listens for ``GET`` (and implicitly ``HEAD``). + to (`GET`, `POST` etc.). By default a rule + just listens for `GET` (and implicitly `HEAD`). + Starting with Flask 0.6, `OPTIONS` is implicitly + added and handled by the standard request handling. :param subdomain: specifies the rule for the subdomain in case subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes @@ -650,7 +667,15 @@ class Flask(_PackageBoundObject): try: if req.routing_exception is not None: raise req.routing_exception - return self.view_functions[req.endpoint](**req.view_args) + rule = req.url_rule + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if rule.provide_automatic_options and req.method == 'OPTIONS': + rv = self.response_class() + rv.allow.update(rule.methods) + return rv + # otherwise dispatch to the handler for that endpoint + return self.view_functions[rule.endpoint](**req.view_args) except HTTPException, e: return self.handle_http_exception(e) diff --git a/flask/ctx.py b/flask/ctx.py index 5ceb1750..854503af 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -38,8 +38,9 @@ class _RequestContext(object): self.flashes = None try: - self.request.endpoint, self.request.view_args = \ - self.url_adapter.match() + url_rule, self.request.view_args = \ + self.url_adapter.match(return_rule=True) + self.request.url_rule = url_rule except HTTPException, e: self.request.routing_exception = e diff --git a/flask/helpers.py b/flask/helpers.py index 420bfab8..cf00528e 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -15,6 +15,7 @@ import posixpath import mimetypes from time import time from zlib import adler32 +from functools import wraps # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. diff --git a/flask/wrappers.py b/flask/wrappers.py index 1dcf23d1..200e7caf 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -24,11 +24,12 @@ class Request(RequestBase): :attr:`~flask.Flask.request_class` to your subclass. """ - #: the endpoint that matched the request. This in combination with - #: :attr:`view_args` can be used to reconstruct the same or a - #: modified URL. If an exception happened when matching, this will - #: be `None`. - endpoint = None + #: the internal URL rule that matched the request. This can be + #: useful to inspect which methods are allowed for the URL from + #: a before/after handler (``request.url_rule.methods``) etc. + #: + #: .. versionadded:: 0.6 + url_rule = None #: a dict of view arguments that matched the request. If an exception #: happened when matching, this will be `None`. @@ -40,6 +41,16 @@ class Request(RequestBase): #: something similar. routing_exception = None + @property + def endpoint(self): + """The endpoint that matched the request. This in combination with + :attr:`view_args` can be used to reconstruct the same or a + modified URL. If an exception happened when matching, this will + be `None`. + """ + if self.url_rule is not None: + return self.url_rule.endpoint + @property def module(self): """The name of the current module""" diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 380ecdad..6c16bbd4 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -111,6 +111,15 @@ class ContextTestCase(unittest.TestCase): class BasicFunctionalityTestCase(unittest.TestCase): + def test_options_work(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + rv = app.test_client().open('/', method='OPTIONS') + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] + assert rv.data == '' + def test_request_dispatching(self): app = flask.Flask(__name__) @app.route('/') @@ -124,7 +133,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/').data == 'GET' rv = c.post('/') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] rv = c.head('/') assert rv.status_code == 200 assert not rv.data # head truncates @@ -132,7 +141,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/more').data == 'GET' rv = c.delete('/more') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] def test_url_mapping(self): app = flask.Flask(__name__) @@ -148,7 +157,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/').data == 'GET' rv = c.post('/') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] rv = c.head('/') assert rv.status_code == 200 assert not rv.data # head truncates @@ -156,7 +165,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/more').data == 'GET' rv = c.delete('/more') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] def test_session(self): app = flask.Flask(__name__)