# (C) Copyright 2005-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

# --(on_trait_change Method Enhancements)--------------------------------------
r"""
on_trait_change Method Enhancements
===================================

In Traits 3.0, the capabilities of the **HasTraits** class's
*on_trait_change* method has been greatly enhanced with the addition of a new
*extended* trait name syntax for specifying the name of the trait the
notification handler applies to.

Previously, the trait name argument to the *on_trait_change* method could only
be one of the following:

Omitted, None, or 'anytrait'
    The notification handler applies to any trait on the object.

name
    The notification handler applies to the trait on the object called *name*.

[name1, ..., namen]
    The notification handler applies to each of the traits on the object with
    the specified names.

In Traits 3.0, all of these forms are still supported, but now the syntax for
specifying *name* has been expanded to allow a much broader set of traits that
are *reachable* from the object the *on_trait_change* method is applied to.

New *name* Parameter Syntax
---------------------------

In Traits 3.0, the *name* parameter, in addition to being omitted, None or
*anytrait*, can now be a single *xname* or a list of *xname* names, where
an *xname* is an extended name of the form::

    xname2['.'xname2]*

An *xname2* is of the form::

   (xname3 | '['xname3[','xname3]*']) ['*']

An *xname3* is of the form::

     xname | ['+'|'-'][name] | name['?' | ('+'|'-')[name]]

A *name* is any valid Python attribute name. The semantic meaning of this
notation is as follows:

[item, item, ..., item]
    A list which matches any of the specified items. Note that at the topmost
    level, the surrounding square brackets are optional.

name?
    If the current object does not have an attribute called *name*, the
    reference can be ignored. If the '?' character is omitted, the current
    object must have a trait called *name*, otherwise an exception will be
    raised.

prefix+
    Matches any trait on the current object whose name begins with *prefix*.

\+metadata_name
    Matches any trait on the current object having *metadata_name* metadata.

\-metadata_name
    Matches any trait on the current object which does not have *metadata_name*
    metadata.

prefix+metadata_name
    Matches any trait on the current object whose name begins with *prefix* and
    which has *metadata_name* metadata.

prefix-metadata_name
    Matches any trait on the current object whose name begins with *prefix* and
    which does not have *metadata_name* metadata.

\+
    Matches all traits on the current object.

pattern*
    Matches object graphs where *pattern* occurs one or more times (useful for
    setting up listeners on recursive data structures like trees or linked
    lists).

Name Syntax Examples
--------------------

Some examples of valid names and their meaning are as follows:

'foo,bar,baz'
    Listen for trait changes to *object.foo*, *object.bar*, and *object.baz*.

['foo','bar','baz']
    Equivalent to 'foo,bar,baz', but may be more useful in cases where the
    individual items are computed.

'foo.bar.baz'
    Listen for trait changes to *object.foo.bar.baz*.

'foo.[bar,baz]'
    Listen for trait changes to *object.foo.bar* and *object.foo.baz*.

'([left,right]).name'
    Listen for trait changes to the *name* trait of each node of a tree having
    *left* and *right* links to other tree nodes, and where *object* is the
    root node of the tree.

'+dirty'
    Listen for trait changes on any trait in the *object* which has the 'dirty'
    metadata set.

'foo.+dirty'
    Listen for trait changes on any trait in *object.foo* which has the 'dirty'
    metadata set.

'foo.[bar,-dirty]'
    Listen for trait changes on *object.foo.bar* or any trait on *object.foo*
    which does not have 'dirty' metadata set.

Additional Semantic Rules
-------------------------

Note that any of the intermediate (i.e., non-final) links in a pattern can be
traits of type **Instance**, **List** or **Dict**. In the case of **List** and
**Dict** traits, the subsequent portion of the pattern is applied to each item
in the list, or value in the dictionary.

For example, if *self.children* is a list, 'children.name' listens for trait
changes to the *name* trait for each item in the *self.children* list.

Also note that items added to or removed from a list or dictionary in the
pattern will cause the *handler* routine to be invoked as well, since this is
treated as an *implied* change to the item's trait being monitored.

Notification Handler Signatures
-------------------------------

The signature of the *handler* supplied also has an effect on how changes to
intermediate traits are processed. The five valid handler signatures are:

1. handler()
2. handler(new)
3. handler(name,new)
4. handler(object,name,new)
5. handler(object,name,old,new)

For signatures 1, 4 and 5, any change to any element of a path being listened
to invokes the handler with information about the particular element that was
modified (e.g., if the item being  monitored is 'foo.bar.baz', a change to
'bar' will call *handler* with the following information:

object
    object.foo
name
    bar
old
    old value for object.foo.bar
new
    new value for object.foo.bar

If one of the intermediate links is a **List** or **Dict**, the call to
*handler* may report an *_items* changed event. If in the previous
example, *bar* is a **List**, and a new item is added to *bar*, then
the information passed to *handler* would be:

object object.foo name bar_items old **Undefined** new **TraitListEvent**
    whose *added* trait contains the new item added to *bar*.

For signatures 2 and 3, the *handler* does not receive enough information to
discern between a change to the final trait being listened to and a change to
an intermediate link. In this case, the event dispatcher will attempt to map a
change to an intermediate link to its effective change on the final trait.
This only works if all of the intermediate links are single values (such as an
**Instance** or **Any** trait) and not **Lists** or **Dicts**. If the modified
intermediate trait or any subsequent intermediate trait preceding the final
trait is a **List** or **Dict**, then a **TraitError** is raised, since the
effective value for the final trait cannot in general be resolved
unambiguously.

Handler signature 1 also has the special characteristic that if a final trait
is a **List** or **Dict**, it will automatically handle *_items* changed
events for the final trait as well. This can be useful in cases where the
*handler* only needs to know that some aspect of the final trait has been
changed. For all other *handler* signatures, you must explicitly specify the
*xxx_items* trait if you want to be notified of changes to any of the items of
the *xxx* trait.

Backward Compatibility
----------------------

The new extended trait name support in Traits 3.0 has one slight semantic
difference with the pre-Traits 3.0 *on_trait_change* method.

Prior to Traits 3.0, it was necessary to make two separate calls to
*on_trait_change* in order to set up listeners on a **List** or **Dict**
trait's value and the contents of its value, as shown in the following
example::

    class Department(HasTraits):

        employees = List(Employee)

        ...

    a_department.on_trait_change(some_listener, 'employees')
    a_department.on_trait_change(some_listener_items, 'employees_items')

In Traits 3.0, this is still the case if the *some_listener* function has one
or more arguments. However, if it has no arguments, the *on_trait_change*
method will automatically call the function either when the trait's value or
its value's contents change. So in Traits 3.0 it is only necessary to write::

    a_department.on_trait_change(some_listener, 'employees')

if the *some_listener* (and *some_listener_items*) function has no arguments.

The net effect of this difference is that code written prior to Traits 3.0
could set up two listeners (e.g. *some_listener* and *some_listener_items*, as
in the example), and then have *both* methods called when the contents of the
trait are modified if the *some_listener* method takes no arguments. Since no
data is passed to the *some_listener* function, there is probably no harm in
doing this, but it does create unnecessary notification handler calls.

As a result, to avoid creating this unwanted overhead in existing code, the
*on_trait_change* method applies pre-Traits 3.0 semantics to all simple names
passed to it (e.g. 'employees'). If you are writing new code and want to
take advantage of the new Traits 3.0 *on_trait_change* semantics for a simple
trait name, you will need to modify the name to use some recognizable aspect of
the new extended trait name syntax.

For example, either of the following lines would cause *new style* semantics to
be applied to the *employees* trait::

    a_department.on_trait_change(some_listener, ' employees')

    a_department.on_trait_change(some_listener, '[employees]')

A Complete Example
------------------

Refer to the code tabs of this lesson for a complete example using
*on_trait_change* with an extended trait name. In particular, check near the
bottom of the **Example** tab for the code that sets up an extended trait
change notification handler using *on_trait_change*.
"""

from traits.api import HasTraits, Int, List, Str


# --[Employee Class]-----------------------------------------------------------
class Employee(HasTraits):

    # The name of the employee:
    name = Str

    # The number of sick days they have taken this year:
    sick_days = Int


# --[Department Class]---------------------------------------------------------
class Department(HasTraits):

    # The name of the department:
    name = Str

    # The employees in the department:
    employees = List(Employee)


# --[Corporation Class]--------------------------------------------------------
class Corporation(HasTraits):

    # The name of the corporation:
    name = Str

    # The departments within the corporation:
    departments = List(Department)


# --[Example*]-----------------------------------------------------------------
# Create some sample employees:
millie = Employee(name="Millie", sick_days=2)
ralph = Employee(name="Ralph", sick_days=3)
tom = Employee(name="Tom", sick_days=1)
slick = Employee(name="Slick", sick_days=16)
marcelle = Employee(name="Marcelle", sick_days=7)
reggie = Employee(name="Reggie", sick_days=11)
dave = Employee(name="Dave", sick_days=0)
bob = Employee(name="Bob", sick_days=1)
alphonse = Employee(name="Alphonse", sick_days=5)

# Create some sample departments:
accounting = Department(name="accounting", employees=[millie, ralph, tom])

sales = Department(name="Sales", employees=[slick, marcelle, reggie])

development = Department(name="Development", employees=[dave, bob, alphonse])

# Create a sample corporation:
acme = Corporation(
    name="Acme, Inc.", departments=[accounting, sales, development]
)


# Define a corporate 'whistle blower' function:
def sick_again(object, name, old, new):
    print(
        "%s just took sick day number %d for this year!" % (object.name, new)
    )


# Set up the function as a listener:
acme.on_trait_change(sick_again, "departments.employees.sick_days")

# Now let's try it out:
slick.sick_days += 1
reggie.sick_days += 1
