summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBert JW Regeer <xistence@0x58.com>2020-01-05 20:02:26 -0800
committerGitHub <noreply@github.com>2020-01-05 20:02:26 -0800
commit5702c3c3a4357a6071c9ba624a89655209548336 (patch)
treebc8d03a5c7927ea3ae2a868fd5af8e1a2cff7c6b
parent148cf5138638ce6b1b92b4e13fe1444df9451e34 (diff)
parent5cad7ad7ce47f1fe151b40ae9398fb5cbbfd3806 (diff)
downloadpyramid-5702c3c3a4357a6071c9ba624a89655209548336.tar.gz
pyramid-5702c3c3a4357a6071c9ba624a89655209548336.tar.bz2
pyramid-5702c3c3a4357a6071c9ba624a89655209548336.zip
Merge pull request #3559 from mmerickel/settable-properties
allow overriding synthesized properties
-rw-r--r--CHANGES.rst7
-rw-r--r--src/pyramid/util.py57
-rw-r--r--tests/test_util.py30
3 files changed, 63 insertions, 31 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 383906e00..70ee43b96 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -71,6 +71,13 @@ Features
- Fix ``DeprecationWarning`` emitted by using the ``imp`` module.
See https://github.com/Pylons/pyramid/pull/3553
+- Properties created via ``config.add_request_method(..., property=True)`` or
+ ``request.set_property`` used to be readonly. They can now be overridden
+ via ``request.foo = ...`` and until the value is deleted it will return
+ the overridden value. This is most useful when mocking request properties
+ in testing.
+ See https://github.com/Pylons/pyramid/pull/3559
+
Deprecations
------------
diff --git a/src/pyramid/util.py b/src/pyramid/util.py
index e552b37de..504516631 100644
--- a/src/pyramid/util.py
+++ b/src/pyramid/util.py
@@ -73,6 +73,26 @@ def as_sorted_tuple(val):
return val
+class SettableProperty(object):
+ def __init__(self, wrapped):
+ self.wrapped = wrapped
+ functools.update_wrapper(self, wrapped)
+
+ def __get__(self, obj, type=None):
+ if obj is None: # pragma: no cover
+ return self
+ value = obj.__dict__.get(self.wrapped.__name__, _marker)
+ if value is _marker:
+ value = self.wrapped(obj)
+ return value
+
+ def __set__(self, obj, value):
+ obj.__dict__[self.wrapped.__name__] = value
+
+ def __delete__(self, obj):
+ del obj.__dict__[self.wrapped.__name__]
+
+
class InstancePropertyHelper(object):
"""A helper object for assigning properties and descriptors to instances.
It is not normally possible to do this because descriptors must be
@@ -94,26 +114,29 @@ class InstancePropertyHelper(object):
(name, property) pair.
"""
- is_property = isinstance(callable, property)
- if is_property:
- fn = callable
- if name is None:
- raise ValueError('must specify "name" for a property')
- if reify:
- raise ValueError('cannot reify a property')
- elif name is not None:
- fn = lambda this: callable(this)
- fn.__name__ = get_callable_name(name)
- fn.__doc__ = callable.__doc__
- else:
+ if name is None:
+ if not hasattr(callable, '__name__'):
+ raise ValueError(
+ 'missing __name__, must specify "name" for property'
+ )
name = callable.__name__
+ name = get_callable_name(name)
+ is_data_descriptor = hasattr(callable, '__set__')
+ if reify and is_data_descriptor:
+ raise ValueError('cannot reify a data descriptor')
+ if is_data_descriptor:
fn = callable
- if reify:
- import pyramid.decorator # avoid circular import
+ else:
+ wrapped = lambda this: callable(this)
+ wrapped.__name__ = name
+ wrapped.__doc__ = callable.__doc__
+
+ if reify:
+ import pyramid.decorator # avoid circular import
- fn = pyramid.decorator.reify(fn)
- elif not is_property:
- fn = property(fn)
+ fn = pyramid.decorator.reify(wrapped)
+ else:
+ fn = SettableProperty(wrapped)
return name, fn
diff --git a/tests/test_util.py b/tests/test_util.py
index 293036c10..1553d8e60 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -103,25 +103,26 @@ class Test_InstancePropertyHelper(unittest.TestCase):
)
def test_override_property(self):
- def worker(obj): # pragma: no cover
+ def worker(obj):
pass
foo = Dummy()
helper = self._getTargetClass()
helper.set_property(foo, worker, name='x')
-
- def doit():
- foo.x = 1
-
- self.assertRaises(AttributeError, doit)
+ self.assertIsNone(foo.x)
+ foo.x = 1
+ self.assertEqual(foo.x, 1)
+ del foo.x
+ self.assertIsNone(foo.x)
def test_override_reify(self):
- def worker(obj): # pragma: no cover
+ def worker(obj):
pass
foo = Dummy()
helper = self._getTargetClass()
helper.set_property(foo, worker, name='x', reify=True)
+ self.assertIsNone(foo.x)
foo.x = 1
self.assertEqual(1, foo.x)
foo.x = 2
@@ -301,23 +302,24 @@ class Test_InstancePropertyMixin(unittest.TestCase):
)
def test_override_property(self):
- def worker(obj): # pragma: no cover
+ def worker(obj):
pass
foo = self._makeOne()
foo.set_property(worker, name='x')
-
- def doit():
- foo.x = 1
-
- self.assertRaises(AttributeError, doit)
+ self.assertIsNone(foo.x)
+ foo.x = 1
+ self.assertEqual(foo.x, 1)
+ del foo.x
+ self.assertIsNone(foo.x)
def test_override_reify(self):
- def worker(obj): # pragma: no cover
+ def worker(obj):
pass
foo = self._makeOne()
foo.set_property(worker, name='x', reify=True)
+ self.assertIsNone(foo.x)
foo.x = 1
self.assertEqual(1, foo.x)
foo.x = 2