Skip to content

Commit

Permalink
All properties should preserve __doc__.
Browse files Browse the repository at this point in the history
Use functools.update_wrapper to preserve everything possible.
  • Loading branch information
jamadden committed Sep 1, 2016
1 parent 2ad1f3d commit 790ef8f
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 15 deletions.
8 changes: 6 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Changes

- Drop support for Python 2.6 and 3.2.

- The properties from the ``property`` module all preserve the
documentation string of the underlying function, and all except
``cachedIn`` preserve everything that ``functools.update_wrapper``
preserves.

4.1.0 (2014-12-26)
------------------
Expand Down Expand Up @@ -36,8 +40,8 @@ Changes
------------------

- Remove dependency on ZODB by allowing to specify storage factory for
``zope.cachedescriptors.method.cachedIn`` which is now `dict` by default.
If you need to use BTree instead, you must pass it as `factory` argument
``zope.cachedescriptors.method.cachedIn`` which is now ``dict`` by default.
If you need to use BTree instead, you must pass it as ``factory`` argument
to the ``zope.cachedescriptors.method.cachedIn`` decorator.

- Remove zpkg-related file.
Expand Down
7 changes: 6 additions & 1 deletion src/zope/cachedescriptors/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
See the CachedProperty class.
"""

from functools import update_wrapper

ncaches = 0


Expand All @@ -27,6 +29,7 @@ def __init__(self, func, *names):
self.data = (func, names,
"_v_cached_property_key_%s" % ncaches,
"_v_cached_property_value_%s" % ncaches)
update_wrapper(self, func)

def __get__(self, inst, class_):
if inst is None:
Expand Down Expand Up @@ -60,7 +63,7 @@ def __init__(self, func, name=None):
if name is None:
name = func.__name__
self.data = (func, name)
self.__doc__ = func.__doc__
update_wrapper(self, func)

def __get__(self, inst, class_):
if inst is None:
Expand All @@ -76,6 +79,7 @@ class readproperty(object):

def __init__(self, func):
self.func = func
update_wrapper(self, func)

def __get__(self, inst, class_):
if inst is None:
Expand All @@ -100,5 +104,6 @@ def get(instance):
value = func(instance)
setattr(instance, self.attribute_name, value)
return value
update_wrapper(get, func)

return property(get)
80 changes: 68 additions & 12 deletions src/zope/cachedescriptors/property.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ persistent objects. Let's look at an example:
>>> import math

>>> class Point:
...
...
... def __init__(self, x, y):
... self.x, self.y = x, y
...
Expand All @@ -26,7 +26,7 @@ persistent objects. Let's look at an example:
... return math.sqrt(self.x**2 + self.y**2)
... radius = property.CachedProperty(radius, 'x', 'y')

>>> point = Point(1.0, 2.0)
>>> point = Point(1.0, 2.0)

If we ask for the radius the first time:

Expand Down Expand Up @@ -60,6 +60,25 @@ Note that we don't have any non-volitile attributes added:
>>> names
['q', 'x', 'y']

Documentation and the ``__name__`` are preserved if the attribute is accessed through
the class. This allows Sphinx to extract the documentation.

>>> class Point:
...
... def __init__(self, x, y):
... self.x, self.y = x, y
...
... def radius(self):
... '''The length of the line between self.x and self.y'''
... print('computing radius')
... return math.sqrt(self.x**2 + self.y**2)
... radius = property.CachedProperty(radius, 'x', 'y')

>>> print(Point.radius.__doc__)
The length of the line between self.x and self.y
>>> print(Point.radius.__name__)
radius


Lazy Computed Attributes
~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -75,7 +94,7 @@ attribute has been computed. Let's look at the previous example using
lazy attributes:

>>> class Point:
...
...
... def __init__(self, x, y):
... self.x, self.y = x, y
...
Expand All @@ -84,7 +103,7 @@ lazy attributes:
... print('computing radius')
... return math.sqrt(self.x**2 + self.y**2)

>>> point = Point(1.0, 2.0)
>>> point = Point(1.0, 2.0)

If we ask for the radius the first time:

Expand All @@ -99,7 +118,7 @@ We see that the radius function is called, but if we ask for it again:

The function isn't called. If we change one of the attribute the
radius depends on, it still isn't called:

>>> point.x = 2.0
>>> '%.2f' % point.radius
'2.24'
Expand Down Expand Up @@ -131,11 +150,11 @@ want to use a different name, we need to pass it:
computing diameter
'5.66'

Documentation is preserved if the attribute is accessed through
Documentation and the ``__name__`` are preserved if the attribute is accessed through
the class. This allows Sphinx to extract the documentation.

>>> class Point:
...
...
... def __init__(self, x, y):
... self.x, self.y = x, y
...
Expand All @@ -147,6 +166,8 @@ the class. This allows Sphinx to extract the documentation.

>>> print(Point.radius.__doc__)
The length of the line between self.x and self.y
>>> print(Point.radius.__name__)
radius

The documentation of the attribute when accessed through the
instance will be the same as the return-value:
Expand All @@ -168,7 +189,7 @@ attribute isn't set by the property:


>>> class Point:
...
...
... def __init__(self, x, y):
... self.x, self.y = x, y
...
Expand All @@ -177,7 +198,7 @@ attribute isn't set by the property:
... print('computing radius')
... return math.sqrt(self.x**2 + self.y**2)

>>> point = Point(1.0, 2.0)
>>> point = Point(1.0, 2.0)

>>> '%.2f' % point.radius
computing radius
Expand All @@ -194,6 +215,24 @@ difference to the builtin `property`:
>>> point.radius
5

Documentation and the ``__name__`` are preserved if the attribute is accessed through
the class. This allows Sphinx to extract the documentation.

>>> class Point:
...
... def __init__(self, x, y):
... self.x, self.y = x, y
...
... @property.readproperty
... def radius(self):
... '''The length of the line between self.x and self.y'''
... print('computing radius')
... return math.sqrt(self.x**2 + self.y**2)

>>> print(Point.radius.__doc__)
The length of the line between self.x and self.y
>>> print(Point.radius.__name__)
radius

cachedIn
~~~~~~~~
Expand All @@ -202,16 +241,16 @@ The `cachedIn` property allows to specify the attribute where to store the
computed value:

>>> class Point:
...
...
... def __init__(self, x, y):
... self.x, self.y = x, y
...
... @property.cachedIn('_radius_attribute')
... def radius(self):
... print('computing radius')
... return math.sqrt(self.x**2 + self.y**2)
>>> point = Point(1.0, 2.0)

>>> point = Point(1.0, 2.0)

>>> '%.2f' % point.radius
computing radius
Expand All @@ -237,3 +276,20 @@ invalidation:

>>> '%.2f' % point.radius
'2.24'

Documentation is preserved if the attribute is accessed through
the class. This allows Sphinx to extract the documentation.

>>> class Point:
...
... def __init__(self, x, y):
... self.x, self.y = x, y
...
... @property.cachedIn('_radius_attribute')
... def radius(self):
... '''The length of the line between self.x and self.y'''
... print('computing radius')
... return math.sqrt(self.x**2 + self.y**2)

>>> print(Point.radius.__doc__)
The length of the line between self.x and self.y

0 comments on commit 790ef8f

Please sign in to comment.