Skip to content

Commit

Permalink
Merge pull request #419 from enthought/has-required-traits
Browse files Browse the repository at this point in the history
Merging #419 (After talking with @corranwebster!)
  • Loading branch information
jjenthought committed Dec 17, 2018
2 parents d0b9472 + f2d61c2 commit 1ab8ed1
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 4 deletions.
2 changes: 2 additions & 0 deletions docs/source/traits_api_reference/has_traits.rst
Expand Up @@ -37,6 +37,8 @@ Classes

.. autoclass:: HasStrictTraits

.. autoclass:: HasRequiredTraits

.. autoclass:: HasPrivateTraits

.. autoclass:: SingletonHasTraits
Expand Down
32 changes: 32 additions & 0 deletions docs/source/traits_user_manual/advanced.rst
Expand Up @@ -301,6 +301,38 @@ exception, as does attempting to set an attribute that is not one of the three
defined attributes. In essence, TreeNode behaves like a type-checked data
structure.

.. index:: HasRequiredTraits class

.. _hasrequiredtraits:

HasRequiredTraits
'''''''''''''''''

This subclass of :ref:`hasstricttraits` ensures that any object attribute with
``required=True`` in its metadata must be passed as an argument on object
initialization.

An example of a class with required traits::

class RequiredTest(HasRequiredTraits):
required_trait = Any(required=True)
non_required_trait = Any()

All required traits have to be provided as arguments on creating a new
instance::

>>> new_instance = RequiredTest(required_trait=13.0)

Non-required traits can also still be provided as usual::

>>> new_instance = RequiredTest(required_trait=13.0, non_required_trait=14.0)

However, omitting a required trait will raise a TraitError::

>>> new_instance = RequiredTest(non_required_trait=14.0)
traits.trait_errors.TraitError: The following required traits were not
provided: required_trait.

.. index:: HasPrivateTraits class

.. _hasprivatetraits:
Expand Down
8 changes: 4 additions & 4 deletions traits/api.py
Expand Up @@ -65,10 +65,10 @@
from .trait_types import UUID, ValidatedTuple

from .has_traits import (HasTraits, HasStrictTraits, HasPrivateTraits,
Interface, SingletonHasTraits, SingletonHasStrictTraits,
SingletonHasPrivateTraits, MetaHasTraits, Vetoable, VetoableEvent,
implements, traits_super, on_trait_change, cached_property,
property_depends_on, provides, isinterface)
HasRequiredTraits, Interface, SingletonHasTraits,
SingletonHasStrictTraits, SingletonHasPrivateTraits, MetaHasTraits,
Vetoable, VetoableEvent, implements, traits_super, on_trait_change,
cached_property, property_depends_on, provides, isinterface)

try:
from .has_traits import ABCHasTraits, ABCHasStrictTraits, ABCMetaHasTraits
Expand Down
51 changes: 51 additions & 0 deletions traits/has_traits.py
Expand Up @@ -3455,6 +3455,57 @@ class HasStrictTraits ( HasTraits ):
"""
_ = Disallow # Disallow access to any traits not explicitly defined

#-------------------------------------------------------------------------------
# 'HasRequiredTraits' class:
#-------------------------------------------------------------------------------

class HasRequiredTraits(HasStrictTraits):
""" This class builds on the functionality of HasStrictTraits and ensures
that any object attribute with `required=True` in its metadata must be
passed as an argument on object initialization.
This can be useful in cases where an object has traits which are required
for it to function correctly.
Raises
------
TraitError
If a required trait is not passed as an argument.
Usage
-----
A class with required traits:
>>> class RequiredTest(HasRequiredTraits):
... required_trait = Any(required=True)
... non_required_trait = Any()
Creating an instance of a HasRequiredTraits subclass:
>>> test_instance = RequiredTest(required_trait=13, non_required_trait=11)
>>> test_instance2 = RequiredTest(required_trait=13)
Forgetting to specify a required trait:
>>> test_instance = RequiredTest(non_required_trait=11)
traits.trait_errors.TraitError: The following required traits were not
provided: required_trait.
"""

def __init__(self, **traits):

missing_required_traits = [
name for name in self.trait_names(required=True)
if name not in traits
]
if missing_required_traits:
raise TraitError(
"The following required traits were not provided: "
"{}.".format(', '.join(sorted(missing_required_traits)))
)

super(HasRequiredTraits, self).__init__(**traits)

#-------------------------------------------------------------------------------
# 'HasPrivateTraits' class:
#-------------------------------------------------------------------------------
Expand Down
30 changes: 30 additions & 0 deletions traits/tests/test_has_required_traits.py
@@ -0,0 +1,30 @@
import unittest
from traits.api import Int, Float, String, HasRequiredTraits, TraitError

class TestHasRequiredTraits(unittest.TestCase):

def test_trait_value_assignment(self):
test_instance = RequiredTest(
i_trait=4, f_trait=2.2, s_trait="test")
self.assertEqual(test_instance.i_trait, 4)
self.assertEqual(test_instance.f_trait, 2.2)
self.assertEqual(test_instance.s_trait, "test")
self.assertEqual(test_instance.non_req_trait, 4.4)
self.assertEqual(test_instance.normal_trait, 42.0)


def test_missing_required_trait(self):
with self.assertRaises(TraitError) as exc:
test_instance = RequiredTest(i_trait=3)
self.assertEqual(
exc.exception.args[0], "The following required traits were not "
"provided: f_trait, s_trait."
)


class RequiredTest(HasRequiredTraits):
i_trait = Int(required=True)
f_trait = Float(required=True)
s_trait = String(required=True)
non_req_trait = Float(4.4, required=False)
normal_trait = Float(42.0)

0 comments on commit 1ab8ed1

Please sign in to comment.