Skip to content

Commit

Permalink
Merge pull request #6 from zopefoundation/cachedproperty-as-decorator
Browse files Browse the repository at this point in the history
Allow using CachedProperty as a decorator in most/all circumstances.
  • Loading branch information
jamadden committed Sep 2, 2016
2 parents 7ffe115 + 8e4a6a3 commit c3ec1f2
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 19 deletions.
11 changes: 9 additions & 2 deletions CHANGES.rst
Expand Up @@ -8,6 +8,13 @@ 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.

- ``property.CachedProperty`` is usable as a decorator, with or
without dependent attribute names.

4.1.0 (2014-12-26)
------------------
Expand Down Expand Up @@ -36,8 +43,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
2 changes: 1 addition & 1 deletion MANIFEST.in
@@ -1,2 +1,2 @@
include *.txt bootstrap.py buildout.cfg tox.ini
include *.txt bootstrap.py buildout.cfg tox.ini *.rst
recursive-include src *.py *.txt *.zcml
43 changes: 40 additions & 3 deletions src/zope/cachedescriptors/property.py
Expand Up @@ -14,11 +14,15 @@
See the CachedProperty class.
"""

from functools import update_wrapper


ncaches = 0


class CachedProperty(object):
"""Cached Properties.
class _CachedProperty(object):
"""
Cached property implementation class.
"""

def __init__(self, func, *names):
Expand All @@ -27,6 +31,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 All @@ -51,6 +56,36 @@ def __get__(self, inst, class_):

return value

def CachedProperty(*args):
"""
CachedProperties.
This is usable directly as a decorator when given names, or when not. Any of these patterns
will work:
* ``@CachedProperty``
* ``@CachedProperty()``
* ``@CachedProperty('n','n2')``
* def thing(self: ...; thing = CachedProperty(thing)
* def thing(self: ...; thing = CachedProperty(thing, 'n')
"""

if not args: # @CachedProperty()
return _CachedProperty # A callable that produces the decorated function

arg1 = args[0]
names = args[1:]
if callable(arg1): # @CachedProperty, *or* thing = CachedProperty(thing, ...)
return _CachedProperty(arg1, *names)

# @CachedProperty( 'n' )
# Ok, must be a list of string names. Which means we are used like a factory
# so we return a callable object to produce the actual decorated function
def factory(function):
return _CachedProperty(function, arg1, *names)
return factory


class Lazy(object):
"""Lazy Attributes.
Expand All @@ -60,7 +95,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 +111,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 +136,6 @@ def get(instance):
value = func(instance)
setattr(instance, self.attribute_name, value)
return value
update_wrapper(get, func)

return property(get)
156 changes: 143 additions & 13 deletions src/zope/cachedescriptors/property.txt
Expand Up @@ -17,16 +17,16 @@ persistent objects. Let's look at an example:
>>> import math

>>> class Point:
...
...
... def __init__(self, x, y):
... self.x, self.y = x, y
...
... @property.CachedProperty('x', 'y')
... def radius(self):
... print('computing radius')
... 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,99 @@ Note that we don't have any non-volitile attributes added:
>>> names
['q', 'x', 'y']

For backwards compatibility, the same thing can alternately be written
without using decorator syntax:

>>> class Point:
...
... def __init__(self, x, y):
... self.x, self.y = x, y
...
... def radius(self):
... print('computing radius')
... return math.sqrt(self.x**2 + self.y**2)
... radius = property.CachedProperty(radius, 'x', 'y')

>>> point = Point(1.0, 2.0)

If we ask for the radius the first time:

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

We see that the radius function is called, but if we ask for it again:

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

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

>>> point.x = 2.0
>>> '%.2f' % point.radius
computing radius
'2.83'

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.CachedProperty('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)

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

It is possible to specify a CachedProperty that has no dependencies.
For backwards compatibility this can be written in a few different ways::

>>> class Point:
... def __init__(self, x, y):
... self.x, self.y = x, y
...
... @property.CachedProperty
... def no_deps_no_parens(self):
... print("No deps, no parens")
... return 1
...
... @property.CachedProperty()
... def no_deps(self):
... print("No deps")
... return 2
...
... def no_deps_old_style(self):
... print("No deps, old style")
... return 3
... no_deps_old_style = property.CachedProperty(no_deps_old_style)


>>> point = Point(1.0, 2.0)
>>> point.no_deps_no_parens
No deps, no parens
1
>>> point.no_deps_no_parens
1
>>> point.no_deps
No deps
2
>>> point.no_deps
2
>>> point.no_deps_old_style
No deps, old style
3
>>> point.no_deps_old_style
3


Lazy Computed Attributes
~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -75,7 +168,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 +177,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 +192,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 +224,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 +240,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 +263,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 +272,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 +289,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 +315,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 +350,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 c3ec1f2

Please sign in to comment.