Pylons / pyramid

Pyramid - A Python web framework
https://trypyramid.com/
Other
3.97k stars 887 forks source link

Add "LIFT" sentinel for context and name arguments to add_view #3739

Open ericatkin opened 11 months ago

ericatkin commented 11 months ago

This change adds a "LIFT" sentinel that signals context and name arguments to add_view should be computed at scan time. This is useful when using venusian.lift to propagate view_config decorators through a view class inheritance hierarchy. LIFT for name in conjunction with view_defaults helps reduces boilerplate and for context it is essential to avoid pyramid.exceptions.ConfigurationConflictError.

I've included an integration test below, but I need help making this into unit tests. Tox is failing for coverage but there is no other breakage.

from pyramid.httpexceptions import HTTPOk
from pyramid.config import Configurator
from pyramid.view import LIFT, view_config, view_defaults
from venusian import lift
from wsgiref.simple_server import make_server

@view_defaults(context=LIFT, name=LIFT)
class A:
    resources = {}

    def __init_subclass__(cls):
        A.resources[cls.__name__] = cls

    def __init__(self, request):
        self.request = request

    def __getitem__(self, key):
        return self.resources[key](self.request)

    @view_config()
    def foo(self):
        return HTTPOk(body=f'{self.__class__.__name__}.foo')

    @view_config()
    def bar(self):
        return HTTPOk(body=f'{self.__class__.__name__}.bar')

A.resources['A'] = A

@lift()
@view_defaults(context=LIFT, name=LIFT)
class B(A):
    @view_config()
    def baz(self):
        return HTTPOk(body=f'{self.__class__.__name__}.baz')

@lift()
@view_defaults(context=LIFT, name=LIFT)
class C(B):
    @view_config()
    def burp(self):
        return HTTPOk(body=f'{self.__class__.__name__}.burp')

    @view_config(name='hiccup')
    def spasm(self):
        return HTTPOk(body=f'{self.__class__.__name__}.spasm')

if __name__ == '__main__':
    with Configurator(root_factory=A) as config:
        config.scan()
        app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 6543, app)
    server.serve_forever()

'''
(
URL=localhost:6543/foo; test `curl -s $URL` = 'A.foo' && echo PASS $URL || echo FAIL $URL
URL=localhost:6543/bar; test `curl -s $URL` = 'A.bar' && echo PASS $URL || echo FAIL $URL
URL=localhost:6543/baz; curl -s -f $URL > /dev/null && echo FAIL $URL || echo PASS $URL
URL=localhost:6543/B/foo; test `curl -s $URL` = 'B.foo' && echo PASS $URL || echo FAIL $URL
URL=localhost:6543/B/bar; test `curl -s $URL` = 'B.bar' && echo PASS $URL || echo FAIL $URL
URL=localhost:6543/B/baz; test `curl -s $URL` = 'B.baz' && echo PASS $URL || echo FAIL $URL
URL=localhost:6543/B/burp; curl -s -f $URL > /dev/null && echo FAIL $URL || echo PASS $URL
URL=localhost:6543/B/C/foo; test `curl -s $URL` = 'C.foo' && echo PASS $URL || echo FAIL $URL
URL=localhost:6543/B/C/bar; test `curl -s $URL` = 'C.bar' && echo PASS $URL || echo FAIL $URL
URL=localhost:6543/B/C/baz; test `curl -s $URL` = 'C.baz' && echo PASS $URL || echo FAIL $URL
URL=localhost:6543/B/C/burp; test `curl -s $URL` = 'C.burp' && echo PASS $URL || echo FAIL $URL
URL=localhost:6543/B/C/hiccup; test `curl -s $URL` = 'C.spasm' && echo PASS $URL || echo FAIL $URL
)
'''
mmerickel commented 10 months ago

can you please target the main branch with this initial PR? once that's merged we'll backport it to the 2.0-branch potentially.

mmerickel commented 10 months ago

Can you paste a version of this code that works without this feature so I can understand it better?

mmerickel commented 10 months ago

tldr seems to be:

Yes? No?

mmerickel commented 10 months ago

If the TLDR is correct it's kind of weird to hide that behavior behind the term "LIFT" as it doesn't seem to really match what lift means in venusian. But I'm learning as I think about this.

ericatkin commented 10 months ago

tldr seems to be:

  • context=LIFT is magic to say "use this class itself as context"
  • view=LIFT is magic to say "use the name of the method as the name of the view"

Yes? No?

Yes, but we need the magic because the class isn't defined until the venusian scan is happening. The context= is applied via view_defaults prior to any subclass that lift is operating on. The name= (you said view=, assumed typo) isn't strictly needed, but reduces the redundancy of repeating the def name for view_config which is very common.

mmerickel commented 9 months ago

None of this is specific to the lift feature though is it?

name=LIFT could just as easily be used on a normal view:

@view_config(name=VALUE_FROM_FUNCTION_NAME)
def foo(request):
    ...

and similarly context=LIFT could just as easily be:

class FooResource:
    def __init__(self, request):
        self.request = request

    @view_config(context=SELF)
    def foo(self):
        ...

I don't think name is really buying us much here and think it should be dropped. I don't really see the benefit considering Pyramid has gone for so long without a way to do this, requiring an explicit name argument.

For context, doing something here is interesting because it's not possible in my example above to use context=FooResource as the class isn't defined yet, so there's a chicken-and-egg issue there similar to python typing using -> Self as a valid return type. https://peps.python.org/pep-0673/

What if we supported typing.Self for this case?

ericatkin commented 9 months ago

None of this is specific to the lift feature though is it?

name=LIFT could just as easily be used on a normal view:

@view_config(name=VALUE_FROM_FUNCTION_NAME)
def foo(request):
    ...

and similarly context=LIFT could just as easily be:

class FooResource:
    def __init__(self, request):
        self.request = request

    @view_config(context=SELF)
    def foo(self):
        ...

I don't think name is really buying us much here and think it should be dropped. I don't really see the benefit considering Pyramid has gone for so long without a way to do this, requiring an explicit name argument.

For context, doing something here is interesting because it's not possible in my example above to use context=FooResource as the class isn't defined yet, so there's a chicken-and-egg issue there similar to python typing using -> Self as a valid return type. https://peps.python.org/pep-0673/

What if we supported typing.Self for this case?

You're right. Not specific to LIFT. That is just how I'll use it and I needed a name. Names are sometimes hard! :)

I like typing.Self. I'll work on a commit for that soon. BTW, I'm trying to get some tests written, but having a hard time. I've been trying to call add_view and just verify that the sentinel was replaced in the view registry, but I don't understand those internals well and haven't yet had success.

Re: name. it could be used on a normal view, but that would be pointless. The value is in using with view_defaults, hence the error if not used on a class method. Now, imagine a class with many dozens of view methods. It is tedious and redundant to have to add the name argument to all the view_config decorators. This is precisely what view_defaults is for IMO. For myself, this functionality is worth the cost, which is a test and some documentation as the implementation is hardly 2 lines and is optional for all those who don't care, but it's not a hill I'll die on. The context sentinel is essential.

I'll get to these changes asap, but it's an after hours project for me :) Thanks.