From bb7fa19d636d999bf844d80939e155b8f212ef3e Mon Sep 17 00:00:00 2001
From: Andreas Maier <maiera@de.ibm.com>
Date: Fri, 23 Jun 2017 19:13:51 +0200
Subject: [PATCH] Made CIMDateTime always timezone-aware, re-enabled
 test_cim_types.py

Details:
- Ensured that 'CIMDateTime' objects for point in time values are
  timezone-aware when supplied with a timezone-naive 'datetime'
  object. This does not change the behavior, but increases code
  clarity.
- Clarified that in the documentation of 'CIMDateTime'.
- Added testcases for CIMDateTime.
- Re-enabled the testing of test_cim_types.py. Since its change
  to using pytest it was not run because the test methods were
  all static methods which apparently does not work. Not sure
  this ever worked.

Signed-off-by: Andreas Maier <maiera@de.ibm.com>
---
 docs/changes.rst            |   5 +
 pywbem/cim_types.py         |  22 +--
 testsuite/test_cim_types.py | 369 +++++++++++++++++++++++---------------------
 3 files changed, 207 insertions(+), 189 deletions(-)

diff --git a/docs/changes.rst b/docs/changes.rst
index 272ed30d..6fdfbcf4 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -72,6 +72,11 @@ Enhancements

 * Added unit test for recorder. See issue #676

+* Ensured that `CIMDateTime` objects for point in time values are
+  timezone-aware when supplied with a timezone-naive `datetime` object.
+  This does not change the behavior, but increases code clarity.
+  Clarified that in the documentation of  `CIMDateTime`. See issue #698.
+
 Bug fixes
 ^^^^^^^^^

diff --git a/pywbem/cim_types.py b/pywbem/cim_types.py
index 6d1f140c..5ecc7707 100644
--- a/pywbem/cim_types.py
+++ b/pywbem/cim_types.py
@@ -74,6 +74,7 @@
 import re
 import warnings
 import six
+import copy

 from . import config

@@ -294,9 +295,11 @@ def __init__(self, dtarg):
             * A :term:`string` object will be
               interpreted as CIM datetime format (see :term:`DSP0004`) and
               will result in a point in time or a time interval.
-            * A :class:`py:datetime.datetime` object must be timezone-aware
-              (see :class:`~pywbem.MinutesFromUTC`) and will result in a point
-              in time.
+            * A :class:`py:datetime.datetime` object will result in a point
+              in time. If the :class:`py:datetime.datetime` object is
+              timezone-aware (see :class:`~pywbem.MinutesFromUTC`), the
+              specified timezone will be used. Otherwise, a default timezone
+              of UTC will be assumed.
             * A :class:`py:datetime.timedelta` object will result in a time
               interval.
             * Another :class:`~pywbem.CIMDateTime` object will be copied.
@@ -342,14 +345,15 @@ def __init__(self, dtarg):
                     raise ValueError('dtarg argument "%s" has an invalid CIM '
                                      'datetime format' % dtarg)
         elif isinstance(dtarg, datetime):
-            self.__datetime = dtarg
+            if dtarg.tzinfo is None:
+                self.__datetime = dtarg.replace(tzinfo=MinutesFromUTC(0))
+            else:
+                self.__datetime = copy.copy(dtarg)
         elif isinstance(dtarg, timedelta):
-            self.__timedelta = dtarg
+            self.__timedelta = copy.copy(dtarg)
         elif isinstance(dtarg, CIMDateTime):
-            # pylint: disable=protected-access
-            self.__datetime = dtarg.__datetime
-            # pylint: disable=protected-access
-            self.__timedelta = dtarg.__timedelta
+            self.__datetime = copy.copy(dtarg.datetime)
+            self.__timedelta = copy.copy(dtarg.timedelta)
         else:
             raise TypeError('dtarg argument "%s" has an invalid type: %s '
                             '(expected datetime, timedelta, string, or '
diff --git a/testsuite/test_cim_types.py b/testsuite/test_cim_types.py
index 4ae354d3..b1f54d06 100755
--- a/testsuite/test_cim_types.py
+++ b/testsuite/test_cim_types.py
@@ -43,105 +43,99 @@ def integer_tuple(request):
     return request.param


-class TestIntegers:
-    """
-    Test CIM integer data type classes.
-    """
-
-    @staticmethod
-    def test_class_attrs_class(integer_tuple):
-        """Test class attrs via class level"""
-        obj_type, exp_cimtype, exp_minvalue, exp_maxvalue = integer_tuple
-        assert obj_type.cimtype == exp_cimtype
-        assert obj_type.minvalue == exp_minvalue
-        assert obj_type.maxvalue == exp_maxvalue
-
-    @staticmethod
-    def test_class_attrs_inst(integer_tuple):
-        """Test class attrs via instance level"""
-        obj_type, exp_cimtype, exp_minvalue, exp_maxvalue = integer_tuple
-        obj = obj_type(42)
-        assert obj.cimtype == exp_cimtype
-        assert obj.minvalue == exp_minvalue
-        assert obj.maxvalue == exp_maxvalue
-
-    @staticmethod
-    def test_inheritance(integer_tuple):
-        """Test inheritance"""
-        obj_type = integer_tuple[0]
-        obj = obj_type(42)
-        assert isinstance(obj, obj_type)
-        assert isinstance(obj, CIMType)
-        assert isinstance(obj, CIMInt)
-        assert not isinstance(obj, CIMFloat)
-
-    @staticmethod
-    def test_init_int(integer_tuple):
-        """Test initialization from integer value"""
-        obj_type = integer_tuple[0]
-        obj = obj_type(42)
-        assert obj == 42
-
-    @staticmethod
-    def test_init_str(integer_tuple):
-        """Test initialization from string value"""
-        obj_type = integer_tuple[0]
-        obj = obj_type('42')
-        assert obj == 42
-
-    @staticmethod
-    def test_init_str_base10(integer_tuple):
-        """Test initialization from string value with base 10"""
-        obj_type = integer_tuple[0]
-        obj = obj_type('42', 10)
-        assert obj == 42
-
-    @staticmethod
-    def test_init_str_base16(integer_tuple):
-        """Test initialization from string value with base 16"""
-        obj_type = integer_tuple[0]
-        obj = obj_type('2A', 16)
-        assert obj == 42
-
-    @staticmethod
-    def test_init_minimum(integer_tuple):
-        """Test initialization from integer value at minimum"""
-        obj_type = integer_tuple[0]
-        exp_minvalue = integer_tuple[2]
-        obj = obj_type(exp_minvalue)
-        assert obj == exp_minvalue
-
-    @staticmethod
-    def test_init_maximum(integer_tuple):
-        """Test initialization from integer value at maximum"""
-        obj_type = integer_tuple[0]
-        exp_maxvalue = integer_tuple[3]
-        obj = obj_type(exp_maxvalue)
-        assert obj == exp_maxvalue
-
-    @staticmethod
-    def test_init_too_low(integer_tuple):
-        """Test initialization from integer value below minimum"""
-        obj_type = integer_tuple[0]
-        exp_minvalue = integer_tuple[2]
-        try:
-            obj_type(exp_minvalue - 1)
-        except ValueError:
-            pass
-        else:
-            raise AssertionError("ValueError was not raised.")
-
-    @staticmethod
-    def test_init_too_high(integer_tuple):
-        """Test initialization from integer value above maximum"""
-        obj_type = integer_tuple[0]
-        exp_maxvalue = integer_tuple[3]
-        try:
-            obj_type(exp_maxvalue + 1)
-        except ValueError:
-            pass
-        else:
-            raise AssertionError("ValueError was not raised.")
+def test_integer_class_attrs_class(integer_tuple):
+    """Test class attrs via class level"""
+    obj_type, exp_cimtype, exp_minvalue, exp_maxvalue = integer_tuple
+    assert obj_type.cimtype == exp_cimtype
+    assert obj_type.minvalue == exp_minvalue
+    assert obj_type.maxvalue == exp_maxvalue
+
+
+def test_integer_class_attrs_inst(integer_tuple):
+    """Test class attrs via instance level"""
+    obj_type, exp_cimtype, exp_minvalue, exp_maxvalue = integer_tuple
+    obj = obj_type(42)
+    assert obj.cimtype == exp_cimtype
+    assert obj.minvalue == exp_minvalue
+    assert obj.maxvalue == exp_maxvalue
+
+
+def test_integer_inheritance(integer_tuple):
+    """Test inheritance"""
+    obj_type = integer_tuple[0]
+    obj = obj_type(42)
+    assert isinstance(obj, obj_type)
+    assert isinstance(obj, CIMType)
+    assert isinstance(obj, CIMInt)
+    assert not isinstance(obj, CIMFloat)
+
+
+def test_integer_init_int(integer_tuple):
+    """Test initialization from integer value"""
+    obj_type = integer_tuple[0]
+    obj = obj_type(42)
+    assert obj == 42
+
+
+def test_integer_init_str(integer_tuple):
+    """Test initialization from string value"""
+    obj_type = integer_tuple[0]
+    obj = obj_type('42')
+    assert obj == 42
+
+
+def test_integer_init_str_base10(integer_tuple):
+    """Test initialization from string value with base 10"""
+    obj_type = integer_tuple[0]
+    obj = obj_type('42', 10)
+    assert obj == 42
+
+
+def test_integer_init_str_base16(integer_tuple):
+    """Test initialization from string value with base 16"""
+    obj_type = integer_tuple[0]
+    obj = obj_type('2A', 16)
+    assert obj == 42
+
+
+def test_integer_init_minimum(integer_tuple):
+    """Test initialization from integer value at minimum"""
+    obj_type = integer_tuple[0]
+    exp_minvalue = integer_tuple[2]
+    obj = obj_type(exp_minvalue)
+    assert obj == exp_minvalue
+
+
+def test_integer_init_maximum(integer_tuple):
+    """Test initialization from integer value at maximum"""
+    obj_type = integer_tuple[0]
+    exp_maxvalue = integer_tuple[3]
+    obj = obj_type(exp_maxvalue)
+    assert obj == exp_maxvalue
+
+
+def test_integer_init_too_low(integer_tuple):
+    """Test initialization from integer value below minimum"""
+    obj_type = integer_tuple[0]
+    exp_minvalue = integer_tuple[2]
+    try:
+        obj_type(exp_minvalue - 1)
+    except ValueError:
+        pass
+    else:
+        raise AssertionError("ValueError was not raised.")
+
+
+def test_integer_init_too_high(integer_tuple):
+    """Test initialization from integer value above maximum"""
+    obj_type = integer_tuple[0]
+    exp_maxvalue = integer_tuple[3]
+    try:
+        obj_type(exp_maxvalue + 1)
+    except ValueError:
+        pass
+    else:
+        raise AssertionError("ValueError was not raised.")


 #
@@ -164,47 +158,41 @@ def real_tuple(request):
     return request.param


-class TestReals:
-    """
-    Test CIM real data type classes.
-    """
-
-    @staticmethod
-    def test_class_attrs_class(real_tuple):
-        """Test class attrs via class level"""
-        obj_type, exp_cimtype = real_tuple
-        assert obj_type.cimtype == exp_cimtype
-
-    @staticmethod
-    def test_class_attrs_inst(real_tuple):
-        """Test class attrs via instance level"""
-        obj_type, exp_cimtype = real_tuple
-        obj = obj_type(42)
-        assert obj.cimtype == exp_cimtype
-
-    @staticmethod
-    def test_inheritance(real_tuple):
-        """Test inheritance"""
-        obj_type = real_tuple[0]
-        obj = obj_type(42)
-        assert isinstance(obj, obj_type)
-        assert isinstance(obj, CIMType)
-        assert isinstance(obj, CIMFloat)
-        assert not isinstance(obj, CIMInt)
-
-    @staticmethod
-    def test_init_float(real_tuple):
-        """Test initialization from floating point value"""
-        obj_type = real_tuple[0]
-        obj = obj_type(42.0)
-        assert obj == 42.0
-
-    @staticmethod
-    def test_init_str(real_tuple):
-        """Test initialization from string value"""
-        obj_type = real_tuple[0]
-        obj = obj_type('42.0')
-        assert obj == 42.0
+def test_real_class_attrs_class(real_tuple):
+    """Test class attrs via class level"""
+    obj_type, exp_cimtype = real_tuple
+    assert obj_type.cimtype == exp_cimtype
+
+
+def test_real_class_attrs_inst(real_tuple):
+    """Test class attrs via instance level"""
+    obj_type, exp_cimtype = real_tuple
+    obj = obj_type(42)
+    assert obj.cimtype == exp_cimtype
+
+
+def test_real_inheritance(real_tuple):
+    """Test inheritance"""
+    obj_type = real_tuple[0]
+    obj = obj_type(42)
+    assert isinstance(obj, obj_type)
+    assert isinstance(obj, CIMType)
+    assert isinstance(obj, CIMFloat)
+    assert not isinstance(obj, CIMInt)
+
+
+def test_real_init_float(real_tuple):
+    """Test initialization from floating point value"""
+    obj_type = real_tuple[0]
+    obj = obj_type(42.0)
+    assert obj == 42.0
+
+
+def test_real_init_str(real_tuple):
+    """Test initialization from string value"""
+    obj_type = real_tuple[0]
+    obj = obj_type('42.0')
+    assert obj == 42.0


 #
@@ -271,6 +259,26 @@ def test_init_str(real_tuple):
         '20140924193040.654321+120'
     ),
     (
+        datetime(year=2014, month=9, day=24, hour=19, minute=30, second=40,
+                 microsecond=654321, tzinfo=MinutesFromUTC(0)),
+        'timestamp',
+        datetime(year=2014, month=9, day=24, hour=19, minute=30, second=40,
+                 microsecond=654321, tzinfo=MinutesFromUTC(0)),
+        None,
+        0,
+        '20140924193040.654321+000'
+    ),
+    (
+        datetime(year=2014, month=9, day=24, hour=19, minute=30, second=40,
+                 microsecond=654321),
+        'timestamp',
+        datetime(year=2014, month=9, day=24, hour=19, minute=30, second=40,
+                 microsecond=654321, tzinfo=MinutesFromUTC(0)),
+        None,
+        0,
+        '20140924193040.654321+000'
+    ),
+    (
         '20140924193040.654321+120',
         'timestamp',
         datetime(year=2014, month=9, day=24, hour=19, minute=30, second=40,
@@ -325,46 +333,47 @@ def datetime_init_tuple(request):
     return request.param


-class TestDatetime:
-    """
-    Test CIM real data type classes.
-    """
-
-    @staticmethod
-    def test_class_attrs_class():
-        """Test class attrs via class level"""
-        assert CIMDateTime.cimtype == 'datetime'
-
-    @staticmethod
-    def test_class_attrs_inst():
-        """Test class attrs via instance level"""
-        obj = CIMDateTime('00000000000000.000000:000')
-        assert obj.cimtype == 'datetime'
-
-    @staticmethod
-    def test_inheritance():
-        """Test inheritance"""
-        obj = CIMDateTime('00000000000000.000000:000')
-        assert isinstance(obj, CIMDateTime)
-        assert isinstance(obj, CIMType)
-        assert not isinstance(obj, CIMFloat)
-        assert not isinstance(obj, CIMInt)
-
-    @staticmethod
-    def test_init(datetime_init_tuple):
-        """Test initialization from all input types"""
-        (dtarg, exp_kind, exp_datetime, exp_timedelta, exp_minutesfromutc,
-         exp_str) = datetime_init_tuple
-        try:
-            obj = CIMDateTime(dtarg)
-        except Exception as exc:
-            assert isinstance(exc, exp_kind)
-        else:
-            assert obj.is_interval == (exp_kind == 'interval')
-            assert obj.datetime == exp_datetime
-            assert obj.timedelta == exp_timedelta
-            assert obj.minutes_from_utc == exp_minutesfromutc
-            assert str(obj) == exp_str
+def test_datetime_class_attrs_class():
+    """Test class attrs via class level"""
+    assert CIMDateTime.cimtype == 'datetime'
+
+
+def test_datetime_class_attrs_inst():
+    """Test class attrs via instance level"""
+    obj = CIMDateTime('00000000000000.000000:000')
+    assert obj.cimtype == 'datetime'
+
+
+def test_datetime_inheritance():
+    """Test inheritance"""
+    obj = CIMDateTime('00000000000000.000000:000')
+    assert isinstance(obj, CIMDateTime)
+    assert isinstance(obj, CIMType)
+    assert not isinstance(obj, CIMFloat)
+    assert not isinstance(obj, CIMInt)
+
+
+def test_datetime_init(datetime_init_tuple):
+    """Test initialization from all input types"""
+    (dtarg, exp_kind, exp_datetime, exp_timedelta, exp_minutesfromutc,
+     exp_str) = datetime_init_tuple
+    try:
+        obj = CIMDateTime(dtarg)
+    except Exception as exc:
+        assert isinstance(exc, exp_kind)
+    else:
+        assert obj.is_interval == (exp_kind == 'interval')
+        assert obj.datetime == exp_datetime
+        if obj.datetime is not None:
+            assert isinstance(obj.datetime, datetime)
+            # We ensure that the datetime is always timezone-aware:
+            assert obj.datetime.tzinfo is not None
+        assert obj.timedelta == exp_timedelta
+        if obj.timedelta is not None:
+            assert isinstance(obj.timedelta, timedelta)
+        assert obj.minutes_from_utc == exp_minutesfromutc
+        assert str(obj) == exp_str
+

 # TODO: Add testcases for get_local_utcoffset()
 # TODO: Add testcases for now()