Customizing Attribute Access
============================

Hooking :meth:`__getattr__`
---------------------------
The __getattr__ method works pretty much the same for persistent
classes as it does for other classes.  No special handling is
needed.  If an object is a ghost, then it will be activated before
__getattr__ is called.

In this example, our objects returns a tuple with the attribute
name, converted to upper case and the value of _p_changed, for any
attribute that isn't handled by the default machinery.

.. doctest::

   >>> from persistent.tests.attrhooks import OverridesGetattr
   >>> o = OverridesGetattr()
   >>> o._p_changed
   False
   >>> o._p_oid
   >>> o._p_jar
   >>> o.spam
   ('SPAM', False)
   >>> o.spam = 1
   >>> o.spam
   1

We'll save the object, so it can be deactivated:

.. doctest::

   >>> from persistent.tests.attrhooks import _resettingJar
   >>> jar = _resettingJar()
   >>> jar.add(o)
   >>> o._p_deactivate()
   >>> o._p_changed

And now, if we ask for an attribute it doesn't have,

.. doctest::

  >>> o.eggs
  ('EGGS', False)

And we see that the object was activated before calling the
:meth:`__getattr__` method.

Hooking All Access
------------------

In this example, we'll provide an example that shows how to
override the :meth:`__getattribute__`, :meth:`__setattr__`, and
:meth:`__delattr__` methods.  We'll create a class that stores it's
attributes in a secret dictionary within the instance dictionary.

The class will have the policy that variables with names starting
with ``tmp_`` will be volatile.

Our sample class takes initial values as keyword arguments to the constructor:

.. doctest::

   >>> from persistent.tests.attrhooks import VeryPrivate
   >>> o = VeryPrivate(x=1)


Hooking :meth:`__getattribute__``
#################################

The :meth:`__getattribute__` method is called for all attribute
accesses.  It overrides the attribute access support inherited
from Persistent.

.. doctest::

   >>> o._p_changed
   False
   >>> o._p_oid
   >>> o._p_jar
   >>> o.x
   1
   >>> o.y
   Traceback (most recent call last):
   ...
   AttributeError: y

Next, we'll save the object in a database so that we can deactivate it:

.. doctest::

   >>> from persistent.tests.attrhooks import _rememberingJar
   >>> jar = _rememberingJar()
   >>> jar.add(o)
   >>> o._p_deactivate()
   >>> o._p_changed

And we'll get some data:

.. doctest::

   >>> o.x
   1

which activates the object:

.. doctest::

   >>> o._p_changed
   False

It works for missing attribes too:

.. doctest::

   >>> o._p_deactivate()
   >>> o._p_changed

   >>> o.y
   Traceback (most recent call last):
   ...
   AttributeError: y

   >>> o._p_changed
   False


Hooking :meth:`__setattr__``
############################

The :meth:`__setattr__` method is called for all attribute
assignments.  It overrides the attribute assignment support
inherited from Persistent.

Implementors of :meth:`__setattr__` methods:

1. Must call Persistent._p_setattr first to allow it
   to handle some attributes and to make sure that the object
   is activated if necessary, and

2. Must set _p_changed to mark objects as changed.

.. doctest::

   >>> o = VeryPrivate()
   >>> o._p_changed
   False
   >>> o._p_oid
   >>> o._p_jar
   >>> o.x
   Traceback (most recent call last):
   ...
   AttributeError: x

   >>> o.x = 1
   >>> o.x
   1

Because the implementation doesn't store attributes directly
in the instance dictionary, we don't have a key for the attribute:

.. doctest::

   >>> 'x' in o.__dict__
   False

Next, we'll give the object a "remembering" jar so we can
deactivate it:

.. doctest::

   >>> jar = _rememberingJar()
   >>> jar.add(o)
   >>> o._p_deactivate()
   >>> o._p_changed

We'll modify an attribute

.. doctest::

   >>> o.y = 2
   >>> o.y
   2

which reactivates it, and markes it as modified, because our
implementation marked it as modified:

.. doctest::

   >>> o._p_changed
   True

Now, if fake a commit:

.. doctest::

   >>> jar.fake_commit()
   >>> o._p_changed
   False

And deactivate the object:

.. doctest::

   >>> o._p_deactivate()
   >>> o._p_changed

and then set a variable with a name starting with ``tmp_``,
The object will be activated, but not marked as modified,
because our :meth:`__setattr__` implementation  doesn't mark the
object as changed if the name starts with ``tmp_``:

.. doctest::

   >>> o.tmp_foo = 3
   >>> o._p_changed
   False
   >>> o.tmp_foo
   3


Hooking :meth:`__delattr__``
############################

The __delattr__ method is called for all attribute
deletions.  It overrides the attribute deletion support
inherited from Persistent.

Implementors of :meth:`__delattr__` methods:

1. Must call Persistent._p_delattr first to allow it
   to handle some attributes and to make sure that the object
   is activated if necessary, and

2. Must set _p_changed to mark objects as changed.

.. doctest::

   >>> o = VeryPrivate(x=1, y=2, tmp_z=3)
   >>> o._p_changed
   False
   >>> o._p_oid
   >>> o._p_jar
   >>> o.x
   1
   >>> del o.x
   >>> o.x
   Traceback (most recent call last):
   ...
   AttributeError: x

Next, we'll save the object in a jar so that we can
deactivate it:

.. doctest::

   >>> jar = _rememberingJar()
   >>> jar.add(o)
   >>> o._p_deactivate()
   >>> o._p_changed

If we delete an attribute:

.. doctest::

   >>> del o.y

The object is activated.  It is also marked as changed because
our implementation marked it as changed.

.. doctest::

   >>> o._p_changed
   True
   >>> o.y
   Traceback (most recent call last):
   ...
   AttributeError: y

   >>> o.tmp_z
   3

Now, if fake a commit:

.. doctest::

   >>> jar.fake_commit()
   >>> o._p_changed
   False

And deactivate the object:

.. doctest::

   >>> o._p_deactivate()
   >>> o._p_changed

and then delete a variable with a name starting with ``tmp_``,
The object will be activated, but not marked as modified,
because our :meth:`__delattr__` implementation  doesn't mark the
object as changed if the name starts with ``tmp_``:

.. doctest::

   >>> del o.tmp_z
   >>> o._p_changed
   False
   >>> o.tmp_z
   Traceback (most recent call last):
   ...
   AttributeError: tmp_z

If we attempt to delete ``_p_oid``, we find that we can't, and the
object is also not activated or changed:

.. doctest::

   >>> del o._p_oid
   Traceback (most recent call last):
   ...
   ValueError: can't delete _p_oid of cached object
   >>> o._p_changed
   False

We are allowed to delete ``_p_changed``, which sets it to ``None``:

   >>> del o._p_changed
   >>> o._p_changed is None
   True
