diff --git a/docs/source/traits_api_reference/trait_types.rst b/docs/source/traits_api_reference/trait_types.rst index 5888db3a9..66a883da7 100644 --- a/docs/source/traits_api_reference/trait_types.rst +++ b/docs/source/traits_api_reference/trait_types.rst @@ -252,7 +252,8 @@ Traits .. autoclass:: WeakRef :show-inheritance: -.. autodata:: Date +.. autoclass:: Date + :show-inheritance: .. autodata:: Datetime diff --git a/traits-stubs/traits-stubs/trait_types.pyi b/traits-stubs/traits-stubs/trait_types.pyi index a2914c976..5728fd071 100644 --- a/traits-stubs/traits-stubs/trait_types.pyi +++ b/traits-stubs/traits-stubs/trait_types.pyi @@ -605,12 +605,14 @@ class WeakRef(Instance): ... -class Date(_BaseInstance[datetime.date]): - # simplified signature +_OptionalDate = Optional[datetime.date] + +class Date(_TraitType[_OptionalDate, _OptionalDate]): def __init__( self, default_value: datetime.date = ..., + allow_datetime: bool = False, **metadata: _Any, ) -> None: ... diff --git a/traits-stubs/traits_stubs_tests/examples/Date.py b/traits-stubs/traits_stubs_tests/examples/Date.py index 23544e367..b7bdb2db4 100644 --- a/traits-stubs/traits_stubs_tests/examples/Date.py +++ b/traits-stubs/traits_stubs_tests/examples/Date.py @@ -20,8 +20,8 @@ class TestClass(HasTraits): obj = TestClass() obj.t = datetime.datetime.now() obj.t = datetime.datetime.now().date() -obj.t = "sometime-string" obj.t = datetime.datetime.now().time() # E: assignment +obj.t = "sometime-string" # E: assignment obj.t = 9 # E: assignment obj.t = [] # E: assignment diff --git a/traits/tests/test_date.py b/traits/tests/test_date.py new file mode 100644 index 000000000..fa494ac66 --- /dev/null +++ b/traits/tests/test_date.py @@ -0,0 +1,102 @@ +# (C) Copyright 2005-2021 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! + +""" +Tests for the Date trait type. +""" + +import datetime +import unittest + +from traits.testing.optional_dependencies import requires_traitsui, traitsui + +from traits.api import Date, HasStrictTraits, TraitError + + +#: Unix epoch date. +UNIX_EPOCH = datetime.date(1970, 1, 1) + +#: Windows NT epoch +NT_EPOCH = datetime.date(1600, 1, 1) + + +class HasDateTraits(HasStrictTraits): + #: Simple case - no default, no parameters, no metadata + simple_date = Date() + + #: Date with default + epoch = Date(UNIX_EPOCH) + + #: Date with default provided via keyword. + alternative_epoch = Date(default_value=NT_EPOCH) + + #: Datetime instances prohibited + datetime_prohibited = Date(allow_datetime=False) + + #: Datetime instances allowed + datetime_allowed = Date(allow_datetime=True) + + +class TestDate(unittest.TestCase): + def test_default(self): + obj = HasDateTraits() + self.assertEqual(obj.simple_date, None) + self.assertEqual(obj.epoch, UNIX_EPOCH) + self.assertEqual(obj.alternative_epoch, NT_EPOCH) + + def test_assign_date(self): + test_date = datetime.date(1975, 2, 13) + obj = HasDateTraits() + obj.simple_date = test_date + self.assertEqual(obj.simple_date, test_date) + + def test_assign_non_date(self): + obj = HasDateTraits() + with self.assertRaises(TraitError) as exception_context: + obj.simple_date = "1975-2-13" + message = str(exception_context.exception) + self.assertIn("must be a date or None", message) + + def test_assign_datetime(self): + # By default, datetime instances are permitted. + test_datetime = datetime.datetime(1975, 2, 13) + obj = HasDateTraits() + obj.simple_date = test_datetime + self.assertEqual(obj.simple_date, test_datetime) + + def test_assign_none(self): + # This is a test for the current behaviour. There may be an argument + # for optionally disallowing None. Note that specifying + # allow_none=False in the trait definition does not work as expected. + # (Ref: enthought/traits#495) + obj = HasDateTraits(simple_date=UNIX_EPOCH) + obj.simple_date = None + self.assertIsNone(obj.simple_date) + + def test_allow_datetime_false(self): + test_datetime = datetime.datetime(1975, 2, 13) + obj = HasDateTraits() + with self.assertRaises(TraitError) as exception_context: + obj.datetime_prohibited = test_datetime + message = str(exception_context.exception) + self.assertIn("must be a non-datetime date or None", message) + + def test_allow_datetime_true(self): + test_datetime = datetime.datetime(1975, 2, 13) + obj = HasDateTraits() + obj.datetime_allowed = test_datetime + self.assertEqual(obj.datetime_allowed, test_datetime) + + @requires_traitsui + def test_get_editor(self): + obj = HasDateTraits() + trait = obj.base_trait("epoch") + editor_factory = trait.get_editor() + self.assertIsInstance(editor_factory, traitsui.api.DateEditor) diff --git a/traits/trait_types.py b/traits/trait_types.py index 050d08530..b977a2ae9 100644 --- a/traits/trait_types.py +++ b/traits/trait_types.py @@ -4238,8 +4238,54 @@ def resolve_class(self, object, name, value): self.klass = klass -#: A trait type for datetime.date instances. -Date = BaseInstance(datetime.date, editor=date_editor) +class Date(TraitType): + """ A trait type whose value must be a date. + + The value must be an instance of :class:`datetime.date`. Note that + :class:`datetime.datetime` is a subclass of :class:`datetime.date`, so + by default instances of :class:`datetime.datetime` are also permitted. + Use ``Date(allow_datetime=False)`` to exclude this possibility. + + Parameters + ---------- + default_value : datetime.date, optional + The default value for this trait. If no default is provided, the + default is ``None``. + allow_datetime : bool, optional + If ``False``, instances of ``datetime.datetime`` are not valid + values for this Trait. The default is ``True``. + **metadata: dict + Additional metadata. + """ + + def __init__(self, default_value=None, *, allow_datetime=True, **metadata): + super().__init__(default_value, **metadata) + self.allow_datetime = allow_datetime + + def validate(self, object, name, value): + """ Check that the given value is valid date for this trait. + """ + if value is None: + return value + if isinstance(value, datetime.date): + if self.allow_datetime or not isinstance(value, datetime.datetime): + return value + + self.error(object, name, value) + + def info(self): + """ + Return text description of this trait. + """ + if self.allow_datetime: + return "a date or None" + else: + return "a non-datetime date or None" + + def create_editor(self): + """ Create default editor factory for this trait. + """ + return date_editor() #: A trait type for datetime.datetime instances.