From 516ce59f95a3b5d2fffbcde8abfdf1951e748361 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Tue, 28 Jun 2016 17:20:25 +0200 Subject: [PATCH 1/2] Add the ability to combine MethodViews --- flask/views.py | 6 +++++- tests/test_views.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/flask/views.py b/flask/views.py index 6e249180..922bb132 100644 --- a/flask/views.py +++ b/flask/views.py @@ -102,12 +102,16 @@ class View(object): return view +def get_methods(cls): + return getattr(cls, 'methods', []) or [] + + class MethodViewType(type): def __new__(cls, name, bases, d): rv = type.__new__(cls, name, bases, d) if 'methods' not in d: - methods = set(rv.methods or []) + methods = set(m for b in bases for m in get_methods(b)) for key in d: if key in http_method_funcs: methods.add(key.upper()) diff --git a/tests/test_views.py b/tests/test_views.py index 8a66bd53..65981dbd 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -160,3 +160,45 @@ def test_endpoint_override(): # But these tests should still pass. We just log a warning. common_test(app) + +def test_multiple_inheritance(): + app = flask.Flask(__name__) + + class GetView(flask.views.MethodView): + def get(self): + return 'GET' + + class DeleteView(flask.views.MethodView): + def delete(self): + return 'DELETE' + + class GetDeleteView(GetView, DeleteView): + pass + + app.add_url_rule('/', view_func=GetDeleteView.as_view('index')) + + c = app.test_client() + assert c.get('/').data == b'GET' + assert c.delete('/').data == b'DELETE' + assert sorted(GetDeleteView.methods) == ['DELETE', 'GET'] + +def test_remove_method_from_parent(): + app = flask.Flask(__name__) + + class GetView(flask.views.MethodView): + def get(self): + return 'GET' + + class OtherView(flask.views.MethodView): + def post(self): + return 'POST' + + class View(GetView, OtherView): + methods = ['GET'] + + app.add_url_rule('/', view_func=View.as_view('index')) + + c = app.test_client() + assert c.get('/').data == b'GET' + assert c.post('/').status_code == 405 + assert sorted(View.methods) == ['GET'] From 648344d4e8878e4aafcc5413f984b21b86173246 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 21 Apr 2017 10:32:00 -0700 Subject: [PATCH 2/2] use mro to collect methods ignore methods attr unless explicitly set add changelog --- CHANGES | 2 ++ flask/views.py | 44 ++++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/CHANGES b/CHANGES index dba0112e..4fd46d71 100644 --- a/CHANGES +++ b/CHANGES @@ -24,8 +24,10 @@ Major release, unreleased - Add support for ``provide_automatic_options`` in ``add_url_rule`` to disable adding OPTIONS method when the ``view_func`` argument is not a class. (`#1489`_). +- ``MethodView`` can inherit method handlers from base classes. (`#1936`_) .. _#1489: https://github.com/pallets/flask/pull/1489 +.. _#1936: https://github.com/pallets/flask/pull/1936 .. _#2017: https://github.com/pallets/flask/pull/2017 .. _#2223: https://github.com/pallets/flask/pull/2223 diff --git a/flask/views.py b/flask/views.py index 757c2a4d..848ccb0b 100644 --- a/flask/views.py +++ b/flask/views.py @@ -102,38 +102,35 @@ class View(object): return view -def get_methods(cls): - return getattr(cls, 'methods', []) or [] - - class MethodViewType(type): + """Metaclass for :class:`MethodView` that determines what methods the view + defines. + """ + + def __init__(cls, name, bases, d): + super(MethodViewType, cls).__init__(name, bases, d) - def __new__(cls, name, bases, d): - rv = type.__new__(cls, name, bases, d) if 'methods' not in d: - methods = set(m for b in bases for m in get_methods(b)) - for key in d: - if key in http_method_funcs: + methods = set() + + for key in http_method_funcs: + if hasattr(cls, key): methods.add(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 base class or another subclass of a base method view - # that does not introduce new methods). + + # 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 base class + # or another subclass of a base method view that does not introduce + # new methods. if methods: - rv.methods = sorted(methods) - return rv + cls.methods = methods class MethodView(with_metaclass(MethodViewType, 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 it will respond to ``'GET'`` requests and - the :meth:`dispatch_request` implementation will automatically - forward your request to that. Also :attr:`options` is set for you - automatically:: + """A class-based view that dispatches request methods to the corresponding + class methods. For example, if you implement a ``get`` method, it will be + used to handle ``GET`` requests. :: class CounterAPI(MethodView): - def get(self): return session.get('counter', 0) @@ -143,11 +140,14 @@ class MethodView(with_metaclass(MethodViewType, View)): app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) """ + def dispatch_request(self, *args, **kwargs): meth = getattr(self, request.method.lower(), None) + # If the request method is HEAD and we don't have a handler for it # retry with GET. if meth is None and request.method == 'HEAD': meth = getattr(self, 'get', None) + assert meth is not None, 'Unimplemented method %r' % request.method return meth(*args, **kwargs)