From 7b3c4adb444725a62038107f31b04eaa47285ba8 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Mon, 1 Feb 2021 17:08:02 +0000 Subject: [PATCH 1/7] Add tests for the 'Date' trait type. --- traits/tests/test_date.py | 89 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 traits/tests/test_date.py diff --git a/traits/tests/test_date.py b/traits/tests/test_date.py new file mode 100644 index 000000000..33f703e53 --- /dev/null +++ b/traits/tests/test_date.py @@ -0,0 +1,89 @@ +# (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): + #: Cake expiry date + expiry = Date(allow_datetime=False) + + #: Datetime allowed! + solstice = Date(allow_datetime=True) + + #: Date with default + epoch = Date(UNIX_EPOCH) + + #: Date with default spelled out explicitly using the keyword. + alternative_epoch = Date(default_value=NT_EPOCH) + + +class TestDate(unittest.TestCase): + def test_default(self): + obj = HasDateTraits() + self.assertEqual(obj.epoch, UNIX_EPOCH) + self.assertEqual(obj.alternative_epoch, NT_EPOCH) + self.assertEqual(obj.expiry, None) + + def test_assign_a_date(self): + test_date = datetime.date(1975, 2, 13) + obj = HasDateTraits() + obj.expiry = test_date + self.assertEqual(obj.expiry, test_date) + + def test_assign_not_a_date(self): + obj = HasDateTraits() + with self.assertRaises(TraitError): + obj.expiry = "1975-2-13" + + def test_info_text(self): + obj = HasDateTraits() + with self.assertRaises(TraitError) as exception_context: + obj.solstice = "1975-2-13" + message = str(exception_context.exception) + self.assertIn("must be a date or None", message) + + 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. + obj = HasDateTraits(expiry=UNIX_EPOCH) + obj.expiry = None + self.assertIsNone(obj.expiry) + + def test_assign_a_datetime_legacy(self): + # Legacy case: by default, datetime instances are permitted. + test_datetime = datetime.datetime(1975, 2, 13) + obj = HasDateTraits() + obj.solstice = test_datetime + self.assertEqual(obj.solstice, 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) From 077e9b9624782108c8bb9274ca11c669177de805 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Mon, 1 Feb 2021 17:09:17 +0000 Subject: [PATCH 2/7] Add test for the proposed new behaviour --- traits/tests/test_date.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/traits/tests/test_date.py b/traits/tests/test_date.py index 33f703e53..58861e360 100644 --- a/traits/tests/test_date.py +++ b/traits/tests/test_date.py @@ -87,3 +87,11 @@ def test_get_editor(self): trait = obj.base_trait("epoch") editor_factory = trait.get_editor() self.assertIsInstance(editor_factory, traitsui.api.DateEditor) + + def test_disallow_datetime(self): + test_datetime = datetime.datetime(1975, 2, 13) + obj = HasDateTraits() + with self.assertRaises(TraitError) as exception_context: + obj.expiry = test_datetime + message = str(exception_context.exception) + self.assertIn("must be a non-datetime date or None", message) From 4bf24b635b5ee507d6e51694a22a7ea8493d306f Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Mon, 1 Feb 2021 17:14:19 +0000 Subject: [PATCH 3/7] Replace Date with a true TraitType subclass, implementing allow_datetime --- traits/trait_types.py | 50 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) 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. From 4b61b1b985fa4bd34b8287371b9c78c60bd7a96f Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Tue, 2 Feb 2021 15:07:24 +0000 Subject: [PATCH 4/7] Test cleanup --- traits/tests/test_date.py | 69 +++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/traits/tests/test_date.py b/traits/tests/test_date.py index 58861e360..fa494ac66 100644 --- a/traits/tests/test_date.py +++ b/traits/tests/test_date.py @@ -28,58 +28,71 @@ class HasDateTraits(HasStrictTraits): - #: Cake expiry date - expiry = Date(allow_datetime=False) - - #: Datetime allowed! - solstice = Date(allow_datetime=True) + #: Simple case - no default, no parameters, no metadata + simple_date = Date() #: Date with default epoch = Date(UNIX_EPOCH) - #: Date with default spelled out explicitly using the keyword. + #: 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) - self.assertEqual(obj.expiry, None) - def test_assign_a_date(self): + def test_assign_date(self): test_date = datetime.date(1975, 2, 13) obj = HasDateTraits() - obj.expiry = test_date - self.assertEqual(obj.expiry, test_date) - - def test_assign_not_a_date(self): - obj = HasDateTraits() - with self.assertRaises(TraitError): - obj.expiry = "1975-2-13" + obj.simple_date = test_date + self.assertEqual(obj.simple_date, test_date) - def test_info_text(self): + def test_assign_non_date(self): obj = HasDateTraits() with self.assertRaises(TraitError) as exception_context: - obj.solstice = "1975-2-13" + 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. - obj = HasDateTraits(expiry=UNIX_EPOCH) - obj.expiry = None - self.assertIsNone(obj.expiry) + # (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_assign_a_datetime_legacy(self): - # Legacy case: by default, datetime instances are permitted. + def test_allow_datetime_true(self): test_datetime = datetime.datetime(1975, 2, 13) obj = HasDateTraits() - obj.solstice = test_datetime - self.assertEqual(obj.solstice, test_datetime) + obj.datetime_allowed = test_datetime + self.assertEqual(obj.datetime_allowed, test_datetime) @requires_traitsui def test_get_editor(self): @@ -87,11 +100,3 @@ def test_get_editor(self): trait = obj.base_trait("epoch") editor_factory = trait.get_editor() self.assertIsInstance(editor_factory, traitsui.api.DateEditor) - - def test_disallow_datetime(self): - test_datetime = datetime.datetime(1975, 2, 13) - obj = HasDateTraits() - with self.assertRaises(TraitError) as exception_context: - obj.expiry = test_datetime - message = str(exception_context.exception) - self.assertIn("must be a non-datetime date or None", message) From 1598a983c8f7d97bc779718210ef640e78642c67 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Tue, 2 Feb 2021 16:20:38 +0000 Subject: [PATCH 5/7] Update stubs for Date --- traits-stubs/traits-stubs/trait_types.pyi | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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: ... From 2f92e128efeb0c63f364b5363dd80154d6c65350 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Tue, 2 Feb 2021 16:23:13 +0000 Subject: [PATCH 6/7] Update API documentation - Date is now a class --- docs/source/traits_api_reference/trait_types.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 2e75f36d847ac07b6165f5f40bae9af4ae4aa2dd Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Tue, 2 Feb 2021 17:10:33 +0000 Subject: [PATCH 7/7] Fix a traits-stubs test: assignment of a string to a Date should not be legal --- traits-stubs/traits_stubs_tests/examples/Date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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