From 184d815a4e37f09405d0c1311ddcf2cc6d2316ea Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Fri, 22 Dec 2023 23:06:47 -0700 Subject: [PATCH 1/8] Add "LIFT" sentinel for context and name arguments to add_view --- src/pyramid/config/views.py | 17 +++++++++++++++-- src/pyramid/view.py | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pyramid/config/views.py b/src/pyramid/config/views.py index 4f5806df35..0b1d3abc51 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 LIFT, AppendSlashNotFoundViewFactory 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.LIFT will cause the :term:`attr` value to be + copied to name (useful with view_defaults to reduce boilerplate). context @@ -587,6 +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 + will cause the :term:`context` value to be set at scan time + (useful in conjunction with venusian :term:`lift`). route_name @@ -815,6 +820,14 @@ def wrapper(context, request): containment = self.maybe_dotted(containment) mapper = self.maybe_dotted(mapper) + if inspect.isclass(view): + if context is LIFT: + 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 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 1541cdb230..0f96fb774f 100644 --- a/src/pyramid/view.py +++ b/src/pyramid/view.py @@ -23,6 +23,7 @@ from pyramid.util import hide_attrs, reraise as reraise_ _marker = object() +LIFT = object() def render_view_to_response(context, request, name='', secure=True): From f4c6d1030a4c4f99991680df6dac5133b89036ba Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Mon, 29 Jan 2024 10:23:41 -0700 Subject: [PATCH 2/8] Use pyramid.util.Sentinel and update changelog --- CHANGES.rst | 3 +++ src/pyramid/config/views.py | 4 ++-- src/pyramid/view.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 46f7fbc18d..96eb1dfc61 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,9 @@ 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 + 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 0b1d3abc51..a6594e607d 100644 --- a/src/pyramid/config/views.py +++ b/src/pyramid/config/views.py @@ -573,8 +573,8 @@ 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 :term:`attr` value to be - copied to name (useful with view_defaults to reduce boilerplate). + the sentinel value view.LIFT will cause the attr value to be copied + to name (useful with view_defaults to reduce boilerplate). context diff --git a/src/pyramid/view.py b/src/pyramid/view.py index 0f96fb774f..0622efae7c 100644 --- a/src/pyramid/view.py +++ b/src/pyramid/view.py @@ -20,10 +20,10 @@ 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() -LIFT = object() +LIFT = Sentinel('LIFT') def render_view_to_response(context, request, name='', secure=True): From 9f29bc37f3a767f1658622e542b8b4d3128eabc6 Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Mon, 24 Jun 2024 15:32:39 -0600 Subject: [PATCH 3/8] 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 96eb1dfc61..b827a30561 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 a6594e607d..5621fdeea3 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 0622efae7c..27860dd2a9 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 c7d8c2721d..80b9f3110d 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 From 02b114fcd47d80b05ff362b3c0a7e3d2a9376426 Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Mon, 24 Jun 2024 15:42:58 -0600 Subject: [PATCH 4/8] Ignore F401 flake8 error for typing.Self --- src/pyramid/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyramid/view.py b/src/pyramid/view.py index 27860dd2a9..23a6561779 100644 --- a/src/pyramid/view.py +++ b/src/pyramid/view.py @@ -27,7 +27,7 @@ if sys.version_info.major < 3 or sys.version_info.minor < 11: Self = Sentinel('Self') # pragma: no cover else: - from typing import Self + from typing import Self # noqa: F401 def render_view_to_response(context, request, name='', secure=True): From 5b696144e783116bc65d3e039c97fcfe5b554aaf Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Mon, 24 Jun 2024 15:49:25 -0600 Subject: [PATCH 5/8] Sort imports --- src/pyramid/config/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyramid/config/views.py b/src/pyramid/config/views.py index 5621fdeea3..2a4fd62e71 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 Self, AppendSlashNotFoundViewFactory +from pyramid.view import AppendSlashNotFoundViewFactory, Self import pyramid.viewderivers from pyramid.viewderivers import ( INGRESS, From a6d06fab26ac72bc4db4d61df56ba19147441fbc Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Thu, 12 Sep 2024 14:00:54 -0600 Subject: [PATCH 6/8] 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 b827a30561..8dddedcc79 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 2a4fd62e71..fad5f7ff6b 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 80b9f3110d..34d3e11a5c 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 From 24f61d4688b9ef08e50fb2b75e35768bcbd1e09f Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Thu, 12 Sep 2024 14:42:12 -0600 Subject: [PATCH 7/8] Fix tests --- tests/test_config/test_views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_config/test_views.py b/tests/test_config/test_views.py index 34d3e11a5c..431e152883 100644 --- a/tests/test_config/test_views.py +++ b/tests/test_config/test_views.py @@ -453,7 +453,7 @@ def view(exc, request): # pragma: no cover config = self._makeOne(autocommit=True) self.assertRaises( ValueError, - lambda: config.add_view(view=view, context=lambda x: x, name=Self), + lambda: config.add_view(view=view, context=lambda x: x), ) def test_add_view_replaces_self(self): @@ -480,6 +480,7 @@ class Foo2: class Foo: # pragma: no cover wrapped = Foo2 + def __init__(self, request): pass @@ -487,9 +488,9 @@ def view(self): pass config = self._makeOne(autocommit=True) - config.add_view(Foo, context=lambda x: x.wrapped, name=Self, attr='view') + config.add_view(Foo, context=lambda x: x.wrapped, attr='view') interface = implementedBy(Foo2) - wrapper = self._getViewCallable(config, interface, name='view') + wrapper = self._getViewCallable(config, interface) self.assertEqual(wrapper.__original_view__, Foo) def test_add_view_context_as_iface(self): From 09f912446603a1d7aaf1a5e118c51ab2cb172700 Mon Sep 17 00:00:00 2001 From: Eric Atkin Date: Thu, 12 Sep 2024 15:14:56 -0600 Subject: [PATCH 8/8] Fix formatting --- src/pyramid/config/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyramid/config/views.py b/src/pyramid/config/views.py index fad5f7ff6b..67e8b42f90 100644 --- a/src/pyramid/config/views.py +++ b/src/pyramid/config/views.py @@ -833,7 +833,8 @@ def wrapper(context, request): 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') + 'context as function is only allowed when view is a class' + ) if is_nonstr_iter(decorator): decorator = combine_decorators(*map(self.maybe_dotted, decorator))