New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

View selection breaks when using multiple base classes #1552

Closed
wichert opened this Issue Feb 2, 2015 · 11 comments

Comments

Projects
None yet
4 participants
@wichert
Member

wichert commented Feb 2, 2015

rest-toolkit uses ABCs to add default behaviour to resources. For example if a resource class adds the ViewableResource abc to its base classes the default GET view from rest-toolkit will be used for that resource. Under the hood this is implemented by adding views for the ABC to the route. The user can then also add a view for the exact resource class to override the default view.

Unfortunately this system breaks if a resource uses multiple ABCs as well as a non-abc base class. This is a test case that demonstrates this:

import abc
from rest_toolkit.compat import add_metaclass  # This is a copy of six.add_metaclass
from webtest import TestApp
from pyramid.config import Configurator


@add_metaclass(abc.ABCMeta)
class Int1(object):
    pass


class OtherBase(object):
    pass


@add_metaclass(abc.ABCMeta)
class Int2(object):
    pass


class Resource(OtherBase, Int1, Int2):
    def __init__(self, request):
        pass


def unknown(context, request):
    return 'unknown method'


def view(context, request):
    return 'hello'


def test_multiple_abcs():
    config = Configurator()
    config.add_route('root', '/', factory=Resource)
    config.add_view(unknown, route_name='root', renderer='string')
    config.add_view(view, route_name='root', context=Int1, request_method='GET', renderer='string')
    config.add_view(view, route_name='root', context=Int2, request_method='POST', renderer='string')
    app = TestApp(config.make_wsgi_app())
    assert 'hello' in app.get('/')
    assert 'hello' in app.post('/')
    assert 'unknown' in app.delete('/')

The failure is:

tests/test_abc.py:40: in test_multiple_abcs
    assert 'hello' in app.post('/')
E   assert 'hello' in <200 OK text/plain body=b'unknown method'>
E    +  where <200 OK text/plain body=b'unknown method'> = <bound method TestApp.post of <webtest.app.TestApp object at 0x111c6e898>>('/')
E    +    where <bound method TestApp.post of <webtest.app.TestApp object at 0x111c6e898>> = <webtest.app.TestApp object at 0x111c6e898>.post

If you remove the OtherBase base class from Resource the test does not fail.

@wichert wichert changed the title from View selection breaks when using both ABCs and normal base classes to View selection breaks when using both multiple base classes Feb 2, 2015

@wichert

This comment has been minimized.

Show comment
Hide comment
@wichert

wichert Feb 2, 2015

Member

The use of ABCs turns out to be a red herring: I get the exact same error if I use normal base classes.

Member

wichert commented Feb 2, 2015

The use of ABCs turns out to be a red herring: I get the exact same error if I use normal base classes.

@wichert

This comment has been minimized.

Show comment
Hide comment
@wichert

wichert Feb 2, 2015

Member

Simplest possible test case:

from webtest import TestApp
from pyramid.config import Configurator

class OtherBase(object): pass
class Int1(object): pass
class Int2(object): pass

class Resource(OtherBase, Int1, Int2):
    def __init__(self, request):
        pass

def unknown(context, request):
    return 'unknown'

def view(context, request):
    return 'hello'

def test_views_for_multiple_base_classes():
    config = Configurator()
    config.add_route('root', '/', factory=Resource)
    config.add_view(unknown, route_name='root', renderer='string')
    config.add_view(view, route_name='root', renderer='string',
            context=Int1, request_method='GET')
    config.add_view(view, route_name='root', renderer='string',
            context=Int2, request_method='POST')
    app = TestApp(config.make_wsgi_app())
    assert 'hello' in app.get('/')
    assert 'hello' in app.post('/')
    assert 'unknown' in app.delete('/')
Member

wichert commented Feb 2, 2015

Simplest possible test case:

from webtest import TestApp
from pyramid.config import Configurator

class OtherBase(object): pass
class Int1(object): pass
class Int2(object): pass

class Resource(OtherBase, Int1, Int2):
    def __init__(self, request):
        pass

def unknown(context, request):
    return 'unknown'

def view(context, request):
    return 'hello'

def test_views_for_multiple_base_classes():
    config = Configurator()
    config.add_route('root', '/', factory=Resource)
    config.add_view(unknown, route_name='root', renderer='string')
    config.add_view(view, route_name='root', renderer='string',
            context=Int1, request_method='GET')
    config.add_view(view, route_name='root', renderer='string',
            context=Int2, request_method='POST')
    app = TestApp(config.make_wsgi_app())
    assert 'hello' in app.get('/')
    assert 'hello' in app.post('/')
    assert 'unknown' in app.delete('/')

@wichert wichert changed the title from View selection breaks when using both multiple base classes to View selection breaks when using multiple base classes Feb 2, 2015

@mmerickel mmerickel added the bugs label Feb 2, 2015

@mmerickel

This comment has been minimized.

Show comment
Hide comment
@mmerickel

mmerickel Feb 2, 2015

Member

Yeah this looks like a bug. I can confirm what you're seeing on 1.5-branch and master.

Member

mmerickel commented Feb 2, 2015

Yeah this looks like a bug. I can confirm what you're seeing on 1.5-branch and master.

@wichert

This comment has been minimized.

Show comment
Hide comment
@wichert

wichert Feb 4, 2015

Member

I seem to have found a workaround: instead of registering the unknown view without a request_method I can register it for all request methods separately:

for method in ['DELETE', 'GET', 'PATCH', 'POST', 'PUT']:
    config.add_view(unknown, route_name='root', request_method=method, renderer='string')
Member

wichert commented Feb 4, 2015

I seem to have found a workaround: instead of registering the unknown view without a request_method I can register it for all request methods separately:

for method in ['DELETE', 'GET', 'PATCH', 'POST', 'PUT']:
    config.add_view(unknown, route_name='root', request_method=method, renderer='string')
@wichert

This comment has been minimized.

Show comment
Hide comment
@wichert

wichert Feb 4, 2015

Member

I spoke too soon: that workaround also doesn't work :(

Member

wichert commented Feb 4, 2015

I spoke too soon: that workaround also doesn't work :(

@sontek

This comment has been minimized.

Show comment
Hide comment
@sontek

sontek Feb 4, 2015

Member

So I believe this is a legit problem with your code and not Pyramid (although an unfortunate one) because the traverser is going to resolve the OtherBase class first, since its the first in the inheritance chain and since that will perfectly match the unknown view it calls it.

If you move the OtherBase inheritance to the end of the chain the code will work because the traverser will match the classes that will hit the other views first.

Member

sontek commented Feb 4, 2015

So I believe this is a legit problem with your code and not Pyramid (although an unfortunate one) because the traverser is going to resolve the OtherBase class first, since its the first in the inheritance chain and since that will perfectly match the unknown view it calls it.

If you move the OtherBase inheritance to the end of the chain the code will work because the traverser will match the classes that will hit the other views first.

@wichert

This comment has been minimized.

Show comment
Hide comment
@wichert

wichert Feb 4, 2015

Member

I don't think this is a problem with my code: the view logic should find the most specific view for the route. In this case it can choose between a view which only filters on route name, and a view which filters on route name, context class and request method, but it still picks the first one.

Reordering the inheritance order is not possible: the other classes are ABCs, and if you list those first their abstract methods will override the real method implementations, breaking the entire class.

Member

wichert commented Feb 4, 2015

I don't think this is a problem with my code: the view logic should find the most specific view for the route. In this case it can choose between a view which only filters on route name, and a view which filters on route name, context class and request method, but it still picks the first one.

Reordering the inheritance order is not possible: the other classes are ABCs, and if you list those first their abstract methods will override the real method implementations, breaking the entire class.

@sontek

This comment has been minimized.

Show comment
Hide comment
@sontek

sontek Feb 4, 2015

Member

@wichert The chunk of code that is causing this is:
https://github.com/Pylons/pyramid/blob/master/pyramid/router.py#L164-L181

You would have to change that chunk to detect most specific rather than going in order

Member

sontek commented Feb 4, 2015

@wichert The chunk of code that is causing this is:
https://github.com/Pylons/pyramid/blob/master/pyramid/router.py#L164-L181

You would have to change that chunk to detect most specific rather than going in order

@sontek

This comment has been minimized.

Show comment
Hide comment
@sontek

sontek Feb 4, 2015

Member

Pushed the fix as a PR

Member

sontek commented Feb 4, 2015

Pushed the fix as a PR

@mcdonc

This comment has been minimized.

Show comment
Hide comment
@mcdonc

mcdonc Apr 6, 2015

Member

This is fixed as the result of #1557

Member

mcdonc commented Apr 6, 2015

This is fixed as the result of #1557

@mcdonc mcdonc closed this Apr 6, 2015

@wichert

This comment has been minimized.

Show comment
Hide comment
@wichert

wichert Apr 7, 2015

Member

I'm very happy to hear that. Thanks!

Member

wichert commented Apr 7, 2015

I'm very happy to hear that. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment