Repository: gmr/pamqp

Issue:
Deprecation warning about `datetime.utcfromtimestamp`
Method [datetime.utcfromtimestamp](https://docs.python.org/3.12/library/datetime.html#datetime.datetime.utcfromtimestamp) is deprecated since 3.12:
```
DeprecationWarning: datetime.datetime.utcfromtimestamp() 
is deprecated and scheduled for removal in a future version. 
Use timezone-aware objects to represent datetimes in UTC: 
datetime.datetime.fromtimestamp(timestamp, datetime.UTC).
```

Discussion:

Patch:
diff --git a/pamqp/decode.py b/pamqp/decode.py
index fd64133..a52ff4f 100644
--- a/pamqp/decode.py
+++ b/pamqp/decode.py
@@ -278,7 +278,8 @@ def timestamp(value: bytes) -> typing.Tuple[int, datetime.datetime]:
     """
     try:
         temp = common.Struct.timestamp.unpack(value[0:8])
-        return 8, datetime.datetime.utcfromtimestamp(temp[0])
+        return 8, datetime.datetime.fromtimestamp(temp[0],
+                                                  tz=datetime.timezone.utc)
     except TypeError:
         raise ValueError('Could not unpack timestamp value')
 


Test patch:
diff --git a/tests/test_decoding.py b/tests/test_decoding.py
index a41be08..436f6dd 100644
--- a/tests/test_decoding.py
+++ b/tests/test_decoding.py
@@ -17,7 +17,8 @@ class CodecDecodeTests(unittest.TestCase):
                  b'\x08')
     FIELD_ARR_VALUE = [
         1, 45000, 'Test ✈',
-        datetime.datetime(2006, 11, 21, 16, 30, 10), -1147483648,
+        datetime.datetime(2006, 11, 21, 16, 30, 10,
+                          tzinfo=datetime.timezone.utc), -1147483648,
         decimal.Decimal('3.14'), 3.14, 3294967295, -9223372036854775800
     ]
     FIELD_TBL = (
@@ -32,7 +33,8 @@ class CodecDecodeTests(unittest.TestCase):
         'intval': 1,
         'strval': 'Test ✈',
         'boolval': True,
-        'timestampval': datetime.datetime(2006, 11, 21, 16, 30, 10),
+        'timestampval': datetime.datetime(2006, 11, 21, 16, 30, 10,
+                                          tzinfo=datetime.timezone.utc),
         'decval': decimal.Decimal('3.14'),
         '🐰': None,
         'floatval': 3.14,
@@ -255,7 +257,8 @@ class CodecDecodeTests(unittest.TestCase):
     def test_decode_timestamp_value(self):
         self.assertEqual(
             decode.timestamp(b'\x00\x00\x00\x00Ec)\x92')[1],
-            datetime.datetime(2006, 11, 21, 16, 30, 10))
+            datetime.datetime(2006, 11, 21, 16, 30, 10,
+                              tzinfo=datetime.timezone.utc))
 
     def test_decode_field_array_bytes_consumed(self):
         self.assertEqual(
@@ -484,7 +487,8 @@ class CodecDecodeTests(unittest.TestCase):
         value = b'\x00\x00\x00\x00Ec)\x92'
         self.assertEqual(
             decode.by_type(value, 'timestamp')[1],
-            datetime.datetime(2006, 11, 21, 16, 30, 10))
+            datetime.datetime(2006, 11, 21, 16, 30, 10,
+                              tzinfo=datetime.timezone.utc))
 
     def test_decode_by_type_void(self):
         self.assertIsNone(decode.by_type(b'', 'void')[1])
@@ -668,7 +672,8 @@ class CodecDecodeTests(unittest.TestCase):
         value = b'T\x00\x00\x00\x00Ec)\x92'
         self.assertEqual(
             decode.embedded_value(value)[1],
-            datetime.datetime(2006, 11, 21, 16, 30, 10))
+            datetime.datetime(2006, 11, 21, 16, 30, 10,
+                              tzinfo=datetime.timezone.utc))
 
     def test_decode_embedded_value_field_array_bytes_consumed(self):
         self.assertEqual(
diff --git a/tests/test_encode_decode.py b/tests/test_encode_decode.py
index a8d4bbd..1788113 100644
--- a/tests/test_encode_decode.py
+++ b/tests/test_encode_decode.py
@@ -19,7 +19,16 @@ class EncodeDecodeTests(unittest.TestCase):
     def test_timestamp_with_dst(self):
         # this test assumes the system is set up using a northern hemisphere
         # timesone with DST (America/New_York as per github CI is fine)
-        data = datetime.datetime(2006, 5, 21, 16, 30, 10)
+        data = datetime.datetime(2006, 5, 21, 16, 30, 10,
+                                 tzinfo=datetime.timezone.utc)
         encoded = encode.timestamp(data)
         decoded = decode.timestamp(encoded)[1]
         self.assertEqual(decoded, data)
+
+    def test_timestamp_without_timezone(self):
+        naive = datetime.datetime(2006, 5, 21, 16, 30, 10)
+        aware = datetime.datetime(2006, 5, 21, 16, 30, 10,
+                                  tzinfo=datetime.timezone.utc)
+        encoded = encode.timestamp(naive)
+        decoded = decode.timestamp(encoded)[1]
+        self.assertEqual(decoded, aware)
diff --git a/tests/test_encoding.py b/tests/test_encoding.py
index 4577c54..856ff6c 100644
--- a/tests/test_encoding.py
+++ b/tests/test_encoding.py
@@ -282,7 +282,8 @@ class MarshalingTests(unittest.TestCase):
     def test_encode_by_type_timestamp(self):
         self.assertEqual(
             encode.by_type(
-                datetime.datetime(2006, 11, 21, 16, 30, 10), 'timestamp'),
+                datetime.datetime(2006, 11, 21, 16, 30, 10,
+                                  tzinfo=datetime.timezone.utc), 'timestamp'),
             b'\x00\x00\x00\x00Ec)\x92')
 
     def test_encode_by_type_field_table(self):
diff --git a/tests/test_frame_marshaling.py b/tests/test_frame_marshaling.py
index 95c5517..cc65ee5 100644
--- a/tests/test_frame_marshaling.py
+++ b/tests/test_frame_marshaling.py
@@ -77,7 +77,8 @@ class MarshalingTests(unittest.TestCase):
             message_type='foo',
             priority=10,
             reply_to='q1',
-            timestamp=datetime.datetime(2019, 12, 19, 23, 29, 00))
+            timestamp=datetime.datetime(2019, 12, 19, 23, 29, 00,
+                                        tzinfo=datetime.timezone.utc))
         expectation = (b'\x02\x00\x01\x00\x00\x00\xa2\x00<\x00\x00\x00\x00\x00'
                        b'\x00\x00\x00\x00\n\xff\xe8\x10application/json\x05'
                        b'bzip2\x00\x00\x00\x11\x03fooS\x00\x00\x00\x08Test '
diff --git a/tests/test_frame_unmarshaling.py b/tests/test_frame_unmarshaling.py
index ae48509..1c24286 100644
--- a/tests/test_frame_unmarshaling.py
+++ b/tests/test_frame_unmarshaling.py
@@ -336,7 +336,8 @@ class DemarshalingTests(unittest.TestCase):
             'reply_to': 'unmarshaling_tests',
             'expiration': '1345274026',
             'message_id': '746a1902-39dc-47cf-9471-9feecda35660',
-            'timestamp': datetime.datetime(2012, 10, 2, 9, 51, 3),
+            'timestamp': datetime.datetime(2012, 10, 2, 9, 51, 3,
+                                           tzinfo=datetime.timezone.utc),
             'message_type': 'unittest',
             'user_id': 'pika',
             'app_id': 'frame_unmarshaling_tests',
@@ -1708,7 +1709,8 @@ class DemarshalingTests(unittest.TestCase):
             message_type='foo',
             priority=10,
             reply_to='q1',
-            timestamp=datetime.datetime(2019, 12, 19, 23, 29, 00))
+            timestamp=datetime.datetime(2019, 12, 19, 23, 29, 00,
+                                        tzinfo=datetime.timezone.utc))
         ch = frame.marshal(header.ContentHeader(0, 10, props), 1)
         rt_props = frame.unmarshal(ch)[2].properties
         self.assertEqual(rt_props, props)