From 9f29bc37f3a767f1658622e542b8b4d3128eabc6 Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Mon, 24 Jun 2024 15:32:39 -0600 Subject: [PATCH] Use typing.Self (when available) as sentinel and cover with tests --- CHANGES.rst | 2 +- src/pyramid/config/views.py | 18 +++++++++--------- src/pyramid/view.py | 6 +++++- tests/test_config/test_views.py | 27 +++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 96eb1dfc6..b827a3056 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,7 +11,7 @@ Features - Coverage reports in tests based on Python 3.11 instead of Python 3.8. -- Added `LIFT` sentinel value that may be used for context and name arguments +- Added `Self` sentinel value that may be used for context and name 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 a6594e607..5621fdeea 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 LIFT, AppendSlashNotFoundViewFactory +from pyramid.view import Self, AppendSlashNotFoundViewFactory import pyramid.viewderivers from pyramid.viewderivers import ( INGRESS, @@ -573,7 +573,7 @@ def wrapper(context, request): The :term:`view name`. Read :ref:`traversal_chapter` to understand the concept of a view name. When :term:`view` is a class, - the sentinel value view.LIFT will cause the attr value to be copied + the sentinel value view.Self will cause the attr value to be copied to name (useful with view_defaults to reduce boilerplate). context @@ -589,9 +589,9 @@ 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.LIFT here + 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 :term:`lift`). + (useful in conjunction with venusian lift). route_name @@ -821,12 +821,12 @@ def wrapper(context, request): mapper = self.maybe_dotted(mapper) if inspect.isclass(view): - if context is LIFT: + if context is Self: context = view - if name is LIFT: - name = attr - elif LIFT in (context, name): - raise ValueError('LIFT is only allowed when view is a class') + if name is Self: + name = attr or "" + elif Self in (context, name): + raise ValueError('Self is only allowed when view is a class') if is_nonstr_iter(decorator): decorator = combine_decorators(*map(self.maybe_dotted, decorator)) diff --git a/src/pyramid/view.py b/src/pyramid/view.py index 0622efae7..27860dd2a 100644 --- a/src/pyramid/view.py +++ b/src/pyramid/view.py @@ -23,7 +23,11 @@ from pyramid.util import Sentinel, hide_attrs, reraise as reraise_ _marker = object() -LIFT = Sentinel('LIFT') + +if sys.version_info.major < 3 or sys.version_info.minor < 11: + Self = Sentinel('Self') # pragma: no cover +else: + from typing import Self def render_view_to_response(context, request, name='', secure=True): diff --git a/tests/test_config/test_views.py b/tests/test_config/test_views.py index c7d8c2721..80b9f3110 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,32 @@ 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_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_context_as_iface(self): from pyramid.renderers import null_renderer