From 7c988a8d02ef9d634e525767351494deba83c37a Mon Sep 17 00:00:00 2001 From: Kevin Duff Date: Wed, 10 Apr 2024 18:52:27 +0100 Subject: [PATCH] Optional Trait - Docs, linting, and another test --- .../traits_api_reference/trait_types.rst | 3 ++ docs/source/traits_user_manual/defining.rst | 43 ++++++++++++++++++- traits/api.pyi | 1 + traits/tests/test_optional.py | 42 +++++++++++++++++- traits/trait_types.py | 2 +- 5 files changed, 87 insertions(+), 4 deletions(-) diff --git a/docs/source/traits_api_reference/trait_types.rst b/docs/source/traits_api_reference/trait_types.rst index f939a0104..9ff450ee6 100644 --- a/docs/source/traits_api_reference/trait_types.rst +++ b/docs/source/traits_api_reference/trait_types.rst @@ -241,6 +241,9 @@ Traits .. autoclass:: Union :show-inheritance: +.. autoclass:: Optional + :show-inheritance: + .. autoclass:: Either :show-inheritance: diff --git a/docs/source/traits_user_manual/defining.rst b/docs/source/traits_user_manual/defining.rst index 9c923acca..5806b599e 100644 --- a/docs/source/traits_user_manual/defining.rst +++ b/docs/source/traits_user_manual/defining.rst @@ -265,7 +265,7 @@ the table. .. index:: Directory(), Disallow, Either(), Enum() .. index:: Event(), Expression(), false, File() .. index:: Instance(), List(), Method(), Module() -.. index:: Password(), Property(), Python() +.. index:: Optional(), Password(), Property(), Python() .. index:: PythonValue(), Range(), ReadOnly(), Regex() .. index:: Set() String(), This, Time() .. index:: ToolbarButton(), true, Tuple(), Type() @@ -355,6 +355,8 @@ the table. +------------------+----------------------------------------------------------+ | Module | Module([\*\*\ *metadata*]) | +------------------+----------------------------------------------------------+ +| Optional | Optional(*trait*\ [, \*\*\ *metadata*]) | ++------------------+----------------------------------------------------------+ | Password | Password([*value* = '', *minlen* = 0, *maxlen* = | | | sys.maxsize, *regex* = '', \*\*\ *metadata*]) | +------------------+----------------------------------------------------------+ @@ -700,6 +702,45 @@ The following example illustrates the difference between `Either` and `Union`:: ... primes = Union([2], None, {'3':6}, 5, 7, 11) ValueError: Union trait declaration expects a trait type or an instance of trait type or None, but got [2] instead +.. index:: Optional trait + +.. _optional: + +Optional +:::::::: +The Optional trait is a shorthand for ``Union(None, *trait*)``. It allows +the value of the trait to be either None or a specified type. The default +value of the trait is None unless specified by ``default_value``. + +.. index:: + pair: Optional trait; examples + +The following is an example of using Optional:: + + # optional.py --- Example of Optional predefined trait + + from traits.api import HasTraits, Optional, Str + + class Person(HasTraits): + name = Str + nickname = Optional(Str) + +This example defines a ``Person`` with a ``name`` and an optional ``nickname``. +Their ``nickname`` can be ``None`` or a string. For example:: + + >>> from traits.api import HasTraits, Optional, Str + >>> class Person(HasTraits): + ... name = Str + ... nickname = Optional(Str) + ... + >>> joseph = Person(name="Joseph") + >>> # Joseph has no nickname + >>> joseph.nickname is None + True + >>> joseph.nickname = "Joe" + >>> joseph.nickname + 'Joe' + .. index:: Either trait .. _either: diff --git a/traits/api.pyi b/traits/api.pyi index 473a7571e..1398a3a3a 100644 --- a/traits/api.pyi +++ b/traits/api.pyi @@ -112,6 +112,7 @@ from .trait_types import ( ToolbarButton as ToolbarButton, Either as Either, Union as Union, + Optional as Optional, Type as Type, Subclass as Subclass, Symbol as Symbol, diff --git a/traits/tests/test_optional.py b/traits/tests/test_optional.py index 51b82e562..f11b4ac1f 100644 --- a/traits/tests/test_optional.py +++ b/traits/tests/test_optional.py @@ -11,7 +11,6 @@ import unittest from traits.api import ( - Bytes, DefaultValue, Float, HasTraits, @@ -21,8 +20,8 @@ Str, TraitError, TraitType, - Type, Optional, + Union, Constant, ) from traits.trait_types import _NoneTrait @@ -234,6 +233,45 @@ def _attribute_changed(self, new): obj.attribute = None self.assertIsNone(obj.shadow_attribute) + def test_optional_nested(self): + """ + You can nest ``Optional``... if you want to + """ + + class TestClass(HasTraits): + attribute = Optional(Optional(Int)) + + self.assertIsNone(TestClass(attribute=None).attribute) + self.assertIsNone(TestClass().attribute) + + obj = TestClass(attribute=3) + + obj.attribute = 5 + self.assertEqual(obj.attribute, 5) + + obj.attribute = None + self.assertIsNone(obj.attribute) + + def test_optional_union_of_optional(self): + """ + ``Union(T1, Optional(T2))`` acts like ``Union(T1, None, T2)`` + """ + class TestClass(HasTraits): + attribute = Union(Int, Optional(Float)) + + self.assertEqual(TestClass(attribute=3).attribute, 3) + self.assertEqual(TestClass(attribute=3.0).attribute, 3.0) + self.assertIsNone(TestClass(attribute=None).attribute) + self.assertEqual(TestClass().attribute, 0) + + a = TestClass(attribute=3) + a.attribute = 5 + self.assertEqual(a.attribute, 5) + a.attribute = 5.0 + self.assertEqual(a.attribute, 5.0) + a.attribute = None + self.assertIsNone(a.attribute) + def test_optional_extend_trait(self): class OptionalOrStr(Optional): def validate(self, obj, name, value): diff --git a/traits/trait_types.py b/traits/trait_types.py index f8cdf02a5..858638c57 100644 --- a/traits/trait_types.py +++ b/traits/trait_types.py @@ -4297,7 +4297,7 @@ class Optional(Union): trait : a trait or value that can be converted using trait_from() The type of item that the set contains. Must not be ``None``. """ - def __init__(self, trait, **metadata): + def __init__(self, trait, **metadata): if trait is None: raise TraitError("Optional type must not be None.")