Skip to content
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

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Features

- Coverage reports in tests based on Python 3.11 instead of Python 3.8.

- Added `Self` sentinel value that may be used for context and name or
function as context arguments to view_config on class methods in conjunction
with venusian lift.

Bug Fixes
---------

Expand Down
25 changes: 23 additions & 2 deletions src/pyramid/config/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
as_sorted_tuple,
is_nonstr_iter,
)
from pyramid.view import AppendSlashNotFoundViewFactory
from pyramid.view import AppendSlashNotFoundViewFactory, Self
import pyramid.viewderivers
from pyramid.viewderivers import (
INGRESS,
Expand Down Expand Up @@ -572,7 +572,9 @@ def wrapper(context, request):
name

The :term:`view name`. Read :ref:`traversal_chapter` to
understand the concept of a view name.
understand the concept of a view name. When :term:`view` is a class,
the sentinel value view.Self will cause the attr value to be copied
to name (useful with view_defaults to reduce boilerplate).

context

Expand All @@ -587,6 +589,11 @@ def wrapper(context, request):
to ``add_view`` as ``for_`` (an older, still-supported
spelling). If the view should *only* match when handling
exceptions, then set the ``exception_only`` to ``True``.
When :term:`view` is a class, the sentinel value view.Self
will cause the :term:`context` value to be set at scan time
(useful in conjunction with venusian lift). It can also be
a function taking the view as it's only argument which return
value will be set as context at scan time.

route_name

Expand Down Expand Up @@ -815,6 +822,20 @@ def wrapper(context, request):
containment = self.maybe_dotted(containment)
mapper = self.maybe_dotted(mapper)

if inspect.isclass(view):
if context is Self:
context = view
elif inspect.isfunction(context):
context = context(view)
if name is Self:
name = attr or ""
elif Self in (context, name):
raise ValueError('Self is only allowed when view is a class')
elif inspect.isfunction(context):
raise ValueError(
'context as function is only allowed when view is a class'
)

if is_nonstr_iter(decorator):
decorator = combine_decorators(*map(self.maybe_dotted, decorator))
else:
Expand Down
7 changes: 6 additions & 1 deletion src/pyramid/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@
IViewClassifier,
)
from pyramid.threadlocal import get_current_registry, manager
from pyramid.util import hide_attrs, reraise as reraise_
from pyramid.util import Sentinel, hide_attrs, reraise as reraise_

_marker = object()

if sys.version_info.major < 3 or sys.version_info.minor < 11:
Self = Sentinel('Self') # pragma: no cover
else:
from typing import Self # noqa: F401

mmerickel marked this conversation as resolved.
Show resolved Hide resolved

def render_view_to_response(context, request, name='', secure=True):
"""Call the :term:`view callable` configured with a :term:`view
Expand Down
58 changes: 58 additions & 0 deletions tests/test_config/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from pyramid.interfaces import IMultiView, IRequest, IResponse
from pyramid.util import text_
from pyramid.view import Self

from . import IDummy, dummy_view

Expand Down Expand Up @@ -435,6 +436,63 @@ class Foo:
wrapper = self._getViewCallable(config, foo)
self.assertEqual(wrapper, view)

def test_add_view_raises_on_self_with_non_class_view(self):
def view(exc, request): # pragma: no cover
pass

config = self._makeOne(autocommit=True)
self.assertRaises(
ValueError,
lambda: config.add_view(view=view, context=Self, name=Self),
)

def test_add_view_raises_on_function_context_with_non_class_view(self):
def view(exc, request): # pragma: no cover
pass

config = self._makeOne(autocommit=True)
self.assertRaises(
ValueError,
lambda: config.add_view(view=view, context=lambda x: x),
)

def test_add_view_replaces_self(self):
from zope.interface import implementedBy

class Foo: # pragma: no cover
def __init__(self, request):
pass

def view(self):
pass

config = self._makeOne(autocommit=True)
config.add_view(Foo, context=Self, name=Self, attr='view')
interface = implementedBy(Foo)
wrapper = self._getViewCallable(config, interface, name='view')
self.assertEqual(wrapper.__original_view__, Foo)

def test_add_view_replaces_function(self):
from zope.interface import implementedBy

class Foo2:
pass

class Foo: # pragma: no cover
wrapped = Foo2

def __init__(self, request):
pass

def view(self):
pass

config = self._makeOne(autocommit=True)
config.add_view(Foo, context=lambda x: x.wrapped, attr='view')
interface = implementedBy(Foo2)
wrapper = self._getViewCallable(config, interface)
self.assertEqual(wrapper.__original_view__, Foo)

def test_add_view_context_as_iface(self):
from pyramid.renderers import null_renderer

Expand Down
Loading