# (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!

# --(Creating New Trait Types)-------------------------------------------------
"""
Creating New Trait Types
========================

You create a *new style* trait type by subclassing the **TraitType** class or
one of its subclasses, such as **Float** or **Str**. **TraitType** provides
the infrastructure for creating a trait type and allows subclasses to define
specific methods and class constants used to create either a new trait *type*
or *property*.

In the next section, we'll cover the methods and class constants used to define
a new trait *type*, and in the section following that we'll show how to define
a new trait *property*.

Defining a New Trait Type
-------------------------

The thing that distinguishes a trait *type* from a *property* is the existence
of a *validate* method in the subclass. The *validate* method should have the
following signature:

validate(self, object, name, value)
    This method validates, coerces, or adapts the specified *value* as the
    value of the *name* trait of the *object* object. This method is called
    when a value is assigned to an object trait that is based on this subclass
    of *TraitType* and the class does not  contain a definition for either the
    *get()* or *set()* methods.

    The method must return the original *value* or any suitably coerced or
    adapted value that is a legal value for the trait. If *value* is  not a
    legal value for the trait, and cannot be coerced or adapted to a legal
    value, the method should either raise a **TraitError** or  call the
    *error()* method to raise a **TraitError** on its behalf.

In addition to *validate*, the subclass can also define the *post_setattr*
method, which should have the following signature:

post_setattr(self, object, name, value)
    This method allows the trait to do additional processing after *value* has
    been successfully assigned to the *name* trait of the *object* object. For
    most traits there is no additional processing that needs to be done, and
    this method need not be defined. It is normally used for creating *shadow*
    (i.e., *mapped* traits), but other uses may arise as well. This method does
    not need to return a value, and should normally not raise any exceptions.

The subclass can also define a constant default value by setting the class-
level *default_value* attribute to the desired constant value. For example::

    class OddInt(BaseInt):

        default_value = 1

        ...

If a non-constant default value is desired, you should override the
**TraitType** class's *get_default_value* method. Refer to the documentation
for the **TraitType** class for more information on what this method does.

If you have a constant string that can be used as the type's *info* value, you
can provide it by simple setting the string as the value of the class-level
*info_text* attribute::

    class OddInt(BaseInt):

        info_text = 'an odd integer'

        ...

If you have a type info string which depends upon the internal state of the
trait, then you should override the **TraitType's** *info* method. This method
has no arguments, and should return a string describing the values accepted by
the trait type (e.g. 'an integer in the range from 1 to 5').

If you also have some type specific initialization that needs to be performed
when the trait type is created, you can also override the **TraitType** class's
*init* method. This method has no arguments and is automatically called from
the **TraitType** class constructor.

If you would like to specify a default Traits UI editor for your new trait
type, you can also override the **TraitType** class's *create_editor* method,
which has no arguments and should return the default **TraitEditor** for any
instances of the type to use.

This provides a basic overview of the basic methods and class constants needed
to define a new trait type. Refer to the complete documentation for the
**TraitType** and **BaseTraitHandler** classes for more information on other
methods that can be overridden if necessary.

Defining a New Trait Property
-----------------------------

You can also define new trait *properties* by subclassing from **TraitType** or
one of its subclasses. A *property* is distinguished from a *type* by the
existence of a *get* and/or *set* method in the **TraitType** subclass. The
signature for these two methods is as follows:

get(self, object, name)
    This is the *getter* method of a trait that behaves like a property. It has
    the following arguments:

    object
        The object that the property applies to.
    name
        The name of the *object* property.

    If this method is not defined, but the *set* method is defined, the trait
    behaves like a *write-only* property. This method should return the value
    of the *name* property for the *object* object.

set(self, object, name, value)
    This is the *setter* method of a trait that behaves like a property. It has
    the following arguments:

    object
        The object the property applies to.
    name
        The name of the property on *object*.
    value
        The value being assigned as the value of the property.

    If this method is not defined, but the *get* method is defined, the trait
    behaves like a *read-only* property. This method does not need to return a
    value, but it should raise a **TraitError** exception if the specified
    *value* is not valid and cannot be  coerced or adapted to a valid value.

Because the value of a *property* is determined by the *get* method, the
*default_value* class constant and *get_default_value* method are not used.

However, all other values and methods, such as the *info_text* class attribute
and *info* method apply to a *property* as well as a normal type. Please refer
to the preceding section on defining a trait type for additional information
that applies to properties as well.
"""

# --<Imports>------------------------------------------------------------------
from random import randint

from traits.api import HasTraits, List, TraitError, TraitType


# --[DiceRoll Type]------------------------------------------------------------
# Define a type whose value represents the roll of a pair of dice:
class DiceRoll(TraitType):

    # Set default value to 'snake-eyes':
    default_value = (1, 1)

    # Describe the type:
    info_text = (
        "a tuple of the form (n,m), where both n and m are integers "
        "in the range from 1 to 6 representing a roll of a pair of "
        "dice"
    )

    # Validate any value assigned to the trait to make sure it is a valid
    # dice roll:
    def validate(self, object, name, value):
        if (
            isinstance(value, tuple)
            and (len(value) == 2)
            and (1 <= value[0] <= 6)
            and (1 <= value[1] <= 6)
        ):
            return value

        self.error(object, name, value)


# --[RandInt Property]---------------------------------------------------------

# Define a read-only property whose value is a random integer in a specified
# range:
class RandInt(TraitType):

    # Define the type's constructor:
    def __init__(self, low=1, high=10, **metadata):
        super().__init__(**metadata)
        self.low = int(low)
        self.high = int(high)

    # Define the property's getter:
    def get(self):
        return randint(self.low, self.high)

    # Define the type's type information:
    def info(self):
        return "a random integer in the range from %d to %d" % (
            self.low,
            self.high,
        )


# --[Craps Class]--------------------------------------------------------------


# Define a test class containing both new trait types/properties:
class Craps(HasTraits):

    rolls = List(DiceRoll)
    die = RandInt(1, 6)


# --[Example*]-----------------------------------------------------------------

# Create a test object:
craps = Craps()

# Add a number of test dice rolls:
for i in range(10):
    craps.rolls.append((craps.die, craps.die))

# Display the results:
print(craps.rolls)

# Try to assign an invalid dice roll:
try:
    craps.rolls.append((0, 0))
except TraitError:
    print("Assigning an invalid dice roll failed.")
