Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a TypedReadOnly trait #488

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions traits/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
Module,
Python,
ReadOnly,
TypedReadOnly,
Disallow,
Constant,
Delegate,
Expand Down
156 changes: 156 additions & 0 deletions traits/tests/test_typed_read_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# ------------------------------------------------------------------------------
#
# Copyright (c) 2019, Enthought, Inc.
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in enthought/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!
#
# Author: Ioannis Tziakos
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's lose the "Author" and "Date" fields; we can track that information through version control.

# Date: 05/24/2019
#
# ------------------------------------------------------------------------------
import unittest

from traits.api import (
HasStrictTraits, Int, List, TraitError, TypedReadOnly, Undefined)


class Dummy(HasStrictTraits):

value_1 = TypedReadOnly(List(Int))

value_2 = TypedReadOnly(Int)

events_1 = List

events_2 = List

def _value_2_default(self):
return 8

def _value_1_changed(self, old, new):
self.events_1.append((old, new))

def _value_2_changed(self, old, new):
self.events_2.append((old, new))


class TestTypedReadOnly(unittest.TestCase):

def test_initialization(self):
# given
dummy = Dummy()

# when/then
self.assertEqual(dummy.events_1, [])
self.assertEqual(dummy.events_2, [])
self.assertEqual(dummy.value_1, [])
self.assertEqual(dummy.value_2, 8)

# when/then
dummy.events_2 = []
with self.assertRaises(TraitError):
dummy.value_2 = 23
self.assertEqual(dummy.events_2, [])

def test_setting_value_at_initialization(self):
# given
dummy = Dummy(value_1=[1, 2, 7], value_2=46)

# when/then
self.assertEqual(dummy.events_1, [(Undefined, [1, 2, 7])])
self.assertEqual(dummy.events_2, [(Undefined, 46)])
self.assertEqual(dummy.value_1, [1, 2, 7])
self.assertEqual(dummy.value_2, 46)

# when/then
dummy.events_2 = []
with self.assertRaises(TraitError):
dummy.value_2 = 23
self.assertEqual(dummy.events_2, [])

# when/then
dummy.events_1 = []
with self.assertRaises(TraitError):
dummy.value_1 = []
self.assertEqual(dummy.events_1, [])

def test_setting_value_after_initialization(self):
# given
dummy = Dummy()

# when
dummy.value_2 = 23

# then
self.assertEqual(dummy.events_2, [(Undefined, 23)])
self.assertEqual(dummy.events_1, [])
self.assertEqual(dummy.value_2, 23)

# when
dummy.value_1 = [9]

# then
self.assertEqual(dummy.events_1, [(Undefined, [9])])
self.assertEqual(dummy.events_2, [(Undefined, 23)])
self.assertEqual(dummy.value_1, [9])

# when/then
dummy.events_2 = []
with self.assertRaises(TraitError):
dummy.value_2 = 23
self.assertEqual(dummy.events_2, [])

# when/then
dummy.events_1 = []
with self.assertRaises(TraitError):
dummy.value_1 = []
self.assertEqual(dummy.events_1, [])

def test_invalid_value(self):
# given
dummy = Dummy()

# when/then
with self.assertRaises(TraitError):
dummy.value_2 = '23'
self.assertEqual(dummy.events_1, [])
self.assertEqual(dummy.events_2, [])

# when/then
with self.assertRaises(TraitError):
dummy.value_1 = [2, 'dff']
self.assertEqual(dummy.events_1, [])
self.assertEqual(dummy.events_2, [])

def test_clone(self):
# given
dummy = Dummy(value_1=[1, 2, 7], value_2=46)
dummy.events_1 = []
dummy.events_2 = []

# when
cloned = dummy.clone_traits()

# then
self.assertEqual(cloned.events_1, [(Undefined, [1, 2, 7])])
self.assertEqual(cloned.events_2, [(Undefined, 46)])
self.assertEqual(cloned.value_1, [1, 2, 7])
self.assertEqual(cloned.value_2, 46)

# when/then
cloned.events_2 = []
with self.assertRaises(TraitError):
cloned.value_2 = 23
self.assertEqual(cloned.events_2, [])

# when/then
cloned.events_1 = []
with self.assertRaises(TraitError):
cloned.value_1 = []
self.assertEqual(cloned.events_1, [])
45 changes: 45 additions & 0 deletions traits/trait_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,51 @@ class ReadOnly(TraitType):
ReadOnly = ReadOnly()


class TypedReadOnly(TraitType):
""" A typed write once read many trait.

The trait allows a compatible value (default is ``Any``) to be
assigned to the attribute if the current value is the
``Undefined`` object. Once any other value is assigned, no
further assignment is allowed. Normally, the initial
assignment to the attribute is performed in the class
constructor, based on information passed to the
constructor. If the read-only value is known in advance of
run-time, use the ``Constant()`` function instead of
``TypedReadOnly`` to define the trait.
"""

def __init__(self, trait=Any, **metadata):
if isinstance(trait, type):
self.inner_traits = (trait(),)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should inner_traits be a method (taking no additional arguments) rather than an attribute? I don't get the behaviour I expect on accessing inner_traits on the corresponding CTrait object:

>>> class A(HasTraits):
...     foo = TypedReadOnly(Int)
...     bar = List(Int)
... 
>>> a = A()
>>> a.traits()["foo"].inner_traits  # gives None
>>> a.traits()["bar"].inner_traits
(<traits.traits.CTrait object at 0x10f4d72c0>,)

else:
self.inner_traits = (trait,)
super(TypedReadOnly, self).__init__(**metadata)

def get(self, object, name, trait):
inner_trait = self.inner_traits[0]
value = inner_trait.get_value(object, name, trait)
if value is Undefined:
return inner_trait.get_default_value()[1]
else:
return value

def set(self, object, name, value):
cname = TraitsCache + name
old = object.__dict__.get(cname, Undefined)
if old is Undefined:
value = self.inner_traits[0].validate(object, name, value)
object.__dict__[cname] = value
object.trait_property_changed(name, old, value)
else:
message = u'Cannot set {!r} of {!r} more than once.'
raise TraitError(message.format(name, object.__class__.__name__))


# Create a singleton instance as the trait:
TypedReadOnly = TypedReadOnly()


class Disallow(TraitType):
""" A trait that prevents any value from being assigned or read.

Expand Down