From a6d06fab26ac72bc4db4d61df56ba19147441fbc Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Thu, 12 Sep 2024 14:00:54 -0600 Subject: [PATCH] Allow a function for view_config(context) (useful with venusian lift) --- CHANGES.rst | 5 +++-- src/pyramid/config/views.py | 9 ++++++++- tests/test_config/test_views.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b827a3056..8dddedcc7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,8 +11,9 @@ 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 arguments - to view_config on class methods in conjunction with venusian lift. +- 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 2a4fd62e7..fad5f7ff6 100644 --- a/src/pyramid/config/views.py +++ b/src/pyramid/config/views.py @@ -591,7 +591,9 @@ def wrapper(context, request): 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). + (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 @@ -823,10 +825,15 @@ def wrapper(context, request): 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)) diff --git a/tests/test_config/test_views.py b/tests/test_config/test_views.py index 80b9f3110..34d3e11a5 100644 --- a/tests/test_config/test_views.py +++ b/tests/test_config/test_views.py @@ -446,6 +446,16 @@ def view(exc, request): # pragma: no cover 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, name=Self), + ) + def test_add_view_replaces_self(self): from zope.interface import implementedBy @@ -462,6 +472,26 @@ def view(self): 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, name=Self, attr='view') + interface = implementedBy(Foo2) + wrapper = self._getViewCallable(config, interface, name='view') + self.assertEqual(wrapper.__original_view__, Foo) + def test_add_view_context_as_iface(self): from pyramid.renderers import null_renderer