================
Smart Properties
================

lzprop
======

The ``@lzprop`` decorator allows the declaration of lazy properties - attributes
that are calculated only on first access, and then just once. This is
extremely useful when working with objects of a limited lifetime (e.g.
adapters) that provide results from expensive calculations.

We use a simple class with one lazy property that tracks the calculation
by printing an informative message:

    >>> from cybertools.util.property import lzprop

    >>> class Demo(object):
    ...     base = 6
    ...     @lzprop
    ...     def value(self):
    ...         print('calculating')
    ...         return self.base * 7

    >>> demo = Demo()

When we first access the `value` attribute the corresponding method will be
called:

    >>> demo.value
    calculating
    42

On subsequent accesses the previously calculated value will be returned:

    >>> demo.value
    42
    >>> demo.base = 15
    >>> demo.value
    42

Let's make sure the value is really calculated upon first access (and not
already during compilation or object creation):

    >>> demo2 = Demo()
    >>> demo2.base = 15
    >>> demo2.value
    calculating
    105
    >>> demo2.value
    105


rwproperty
==========

:Author:   Philipp von Weitershausen
:Email:    philikon@philikon.de
:License:  Zope Public License, v2.1

Goal
----

There should be a way to declare a read & write property and still use
the compact and easy decorator spelling.  The read & write properties
should be as easy to use as the read-only property.  We explicitly
don't want that immediately called function that really just helps us
name the attribute and create a local scope for the getter and setter.


Read & write property
---------------------

Read & write properties work like regular properties.  You simply
define a method and then apply a decorator, except that you now don't
use ``@property`` but ``@getproperty`` to mark the getter and
``@setproperty`` to mark the setter:

  >>> from cybertools.util.property import getproperty, setproperty, delproperty

  >>> class JamesBrown(object):
  ...     @getproperty
  ...     def feel(self):
  ...         return self._feel
  ...     @setproperty
  ...     def feel(self, feel):
  ...         self._feel = feel

  >>> i = JamesBrown()
  >>> try: i.feel
  ... except Exception as e: print(e)
  'JamesBrown' object has no attribute '_feel'

  >>> i.feel = "good"
  >>> i.feel
  'good'

The order in which getters and setters are declared doesn't matter:

  >>> class JamesBrown(object):
  ...     @setproperty
  ...     def feel(self, feel):
  ...         self._feel = feel
  ...     @getproperty
  ...     def feel(self):
  ...         return self._feel

  >>> i = JamesBrown()
  >>> i.feel = "good"
  >>> i.feel
  'good'

Of course, deleters are also possible:

  >>> class JamesBrown(object):
  ...     @setproperty
  ...     def feel(self, feel):
  ...         self._feel = feel
  ...     @getproperty
  ...     def feel(self):
  ...         return self._feel
  ...     @delproperty
  ...     def feel(self):
  ...         del self._feel

  >>> i = JamesBrown()
  >>> i.feel = "good"
  >>> del i.feel
  >>> try: i.feel
  ... except Exception as e: print(e)
  'JamesBrown' object has no attribute '_feel'


Edge cases
----------

There might be a case where you're using a flavour of read & write
properties and already have a non-property attribute of the same name
defined:

  >>> class JamesBrown(object):
  ...     feel = "good"
  ...     @getproperty
  ...     def feel(self):
  ...         return "so good"
  ...
  Traceback (most recent call last):
  ...
  TypeError: read & write properties cannot be mixed with other attributes except regular property objects.
