diff --git a/CHANGES.rst b/CHANGES.rst index 46f7fbc18..8dddedcc7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 --------- diff --git a/src/pyramid/config/views.py b/src/pyramid/config/views.py index 4f5806df3..67e8b42f9 100644 --- a/src/pyramid/config/views.py +++ b/src/pyramid/config/views.py @@ -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, @@ -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 @@ -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 @@ -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: diff --git a/src/pyramid/view.py b/src/pyramid/view.py index 1541cdb23..23a656177 100644 --- a/src/pyramid/view.py +++ b/src/pyramid/view.py @@ -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 + def render_view_to_response(context, request, name='', secure=True): """Call the :term:`view callable` configured with a :term:`view diff --git a/tests/test_config/test_views.py b/tests/test_config/test_views.py index c7d8c2721..431e15288 100644 --- a/tests/test_config/test_views.py +++ b/tests/test_config/test_views.py @@ -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 @@ -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