From 06787805c1e8de536fbc558818c3f50da36f7fc1 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 28 Feb 2024 10:04:33 -0400 Subject: [PATCH 1/3] Fix datetime conversion when tzinfo is used --- src/embed_tests/TestConverter.cs | 50 +++++++++++++++++++++++++++++++- src/runtime/Converter.cs | 11 ++++--- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/embed_tests/TestConverter.cs b/src/embed_tests/TestConverter.cs index 9acfbe42d..e86b7f651 100644 --- a/src/embed_tests/TestConverter.cs +++ b/src/embed_tests/TestConverter.cs @@ -187,6 +187,54 @@ public void ConvertDateTimeRoundTrip(DateTimeKind kind) Assert.AreEqual(datetime, result); } + [TestCase("", DateTimeKind.Unspecified)] + [TestCase("America/New_York", DateTimeKind.Unspecified)] + [TestCase("UTC", DateTimeKind.Utc)] + public void ConvertDateTimeWithTimeZonePythonToCSharp(string timeZone, DateTimeKind expectedDateTimeKind) + { + const int year = 2024; + const int month = 2; + const int day = 27; + const int hour = 12; + const int minute = 30; + const int second = 45; + + using (Py.GIL()) + { + dynamic module = PyModule.FromString("module", @$" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") +AddReference(""System"") + +from Python.EmbeddingTest import * + +from datetime import datetime +from pytz import timezone + +tzinfo = timezone('{timeZone}') if '{timeZone}' else None + +def GetPyDateTime(): + return datetime({year}, {month}, {day}, {hour}, {minute}, {second}, tzinfo=tzinfo) \ + if tzinfo else \ + datetime({year}, {month}, {day}, {hour}, {minute}, {second}) + +def GetNextDay(dateTime): + return TestConverter.GetNextDay(dateTime) +"); + + var pyDateTime = module.GetPyDateTime(); + var dateTimeResult = default(object); + + Assert.DoesNotThrow(() => Converter.ToManaged(pyDateTime, typeof(DateTime), out dateTimeResult, false)); + + var managedDateTime = (DateTime)dateTimeResult; + + var expectedDateTime = new DateTime(year, month, day, hour, minute, second); + Assert.AreEqual(expectedDateTime, managedDateTime); + Assert.AreEqual(managedDateTime.Kind, expectedDateTimeKind); + } + } + [Test] public void ConvertTimestampRoundTrip() { @@ -362,7 +410,7 @@ class PyGetListImpl(test.GetListImpl): List result = inst.GetList(); CollectionAssert.AreEqual(new[] { "testing" }, result); } - + [Test] public void PrimitiveIntConversion() { diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs index 05afe2f38..fb7f7071b 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -76,7 +76,6 @@ static Converter() timeSpanCtor = Runtime.PyObject_GetAttrString(dateTimeMod.Borrow(), "timedelta").MoveToPyObject(); PythonException.ThrowIfIsNull(timeSpanCtor); - tzInfoCtor = new Lazy(() => { var tzInfoMod = PyModule.FromString("custom_tzinfo", @" @@ -1131,9 +1130,13 @@ internal static bool ToPrimitive(BorrowedReference value, Type obType, out objec NewReference minutes = default; if (!tzinfo.IsNone() && !tzinfo.IsNull()) { - hours = Runtime.PyObject_GetAttrString(tzinfo.Borrow(), hoursPtr); - minutes = Runtime.PyObject_GetAttrString(tzinfo.Borrow(), minutesPtr); - if (Runtime.PyLong_AsLong(hours.Borrow()) == 0 && Runtime.PyLong_AsLong(minutes.Borrow()) == 0) + var tznameMethod = Runtime.PyObject_GetAttrString(tzinfo.Borrow(), new StrPtr("tzname", Encoding.UTF8)); + var args = Runtime.PyTuple_New(1); + Runtime.PyTuple_SetItem(args.Borrow(), 0, Runtime.None.Steal()); + var tznameObj = Runtime.PyObject_CallObject(tznameMethod.Borrow(), args.Borrow()); + var tzname = Runtime.GetManagedString(tznameObj.Borrow()); + + if (tzname.Contains("UTC", StringComparison.InvariantCultureIgnoreCase)) { timeKind = DateTimeKind.Utc; } From 363b3524aa05e4aea45b1f425f7f1bb7885e3e3e Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 28 Feb 2024 10:36:58 -0400 Subject: [PATCH 2/3] Remove tzinfo check in datetime conversion --- src/embed_tests/TestConverter.cs | 9 ++++----- src/runtime/Converter.cs | 31 +------------------------------ 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/src/embed_tests/TestConverter.cs b/src/embed_tests/TestConverter.cs index e86b7f651..3d68456e3 100644 --- a/src/embed_tests/TestConverter.cs +++ b/src/embed_tests/TestConverter.cs @@ -187,10 +187,10 @@ public void ConvertDateTimeRoundTrip(DateTimeKind kind) Assert.AreEqual(datetime, result); } - [TestCase("", DateTimeKind.Unspecified)] - [TestCase("America/New_York", DateTimeKind.Unspecified)] - [TestCase("UTC", DateTimeKind.Utc)] - public void ConvertDateTimeWithTimeZonePythonToCSharp(string timeZone, DateTimeKind expectedDateTimeKind) + [TestCase("")] + [TestCase("America/New_York")] + [TestCase("UTC")] + public void ConvertDateTimeWithTimeZonePythonToCSharp(string timeZone) { const int year = 2024; const int month = 2; @@ -231,7 +231,6 @@ def GetNextDay(dateTime): var expectedDateTime = new DateTime(year, month, day, hour, minute, second); Assert.AreEqual(expectedDateTime, managedDateTime); - Assert.AreEqual(managedDateTime.Kind, expectedDateTimeKind); } } diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs index fb7f7071b..3f249f0aa 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -1123,24 +1123,6 @@ internal static bool ToPrimitive(BorrowedReference value, Type obType, out objec var minute = Runtime.PyObject_GetAttrString(value, minutePtr); var second = Runtime.PyObject_GetAttrString(value, secondPtr); var microsecond = Runtime.PyObject_GetAttrString(value, microsecondPtr); - var timeKind = DateTimeKind.Unspecified; - var tzinfo = Runtime.PyObject_GetAttrString(value, tzinfoPtr); - - NewReference hours = default; - NewReference minutes = default; - if (!tzinfo.IsNone() && !tzinfo.IsNull()) - { - var tznameMethod = Runtime.PyObject_GetAttrString(tzinfo.Borrow(), new StrPtr("tzname", Encoding.UTF8)); - var args = Runtime.PyTuple_New(1); - Runtime.PyTuple_SetItem(args.Borrow(), 0, Runtime.None.Steal()); - var tznameObj = Runtime.PyObject_CallObject(tznameMethod.Borrow(), args.Borrow()); - var tzname = Runtime.GetManagedString(tznameObj.Borrow()); - - if (tzname.Contains("UTC", StringComparison.InvariantCultureIgnoreCase)) - { - timeKind = DateTimeKind.Utc; - } - } var convertedHour = 0L; var convertedMinute = 0L; @@ -1161,8 +1143,7 @@ internal static bool ToPrimitive(BorrowedReference value, Type obType, out objec (int)convertedHour, (int)convertedMinute, (int)convertedSecond, - millisecond: (int)milliseconds, - timeKind); + (int)milliseconds); year.Dispose(); month.Dispose(); @@ -1172,16 +1153,6 @@ internal static bool ToPrimitive(BorrowedReference value, Type obType, out objec second.Dispose(); microsecond.Dispose(); - if (!tzinfo.IsNull()) - { - tzinfo.Dispose(); - if (!tzinfo.IsNone()) - { - hours.Dispose(); - minutes.Dispose(); - } - } - Exceptions.Clear(); return true; default: From 3b0f31eb4d7378551902b87caef10c225d3f056d Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 28 Feb 2024 10:47:49 -0400 Subject: [PATCH 3/3] Bumped version to 2.0.27 --- src/perf_tests/Python.PerformanceTests.csproj | 4 ++-- src/runtime/Properties/AssemblyInfo.cs | 4 ++-- src/runtime/Python.Runtime.csproj | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index d081af07c..373383145 100644 --- a/src/perf_tests/Python.PerformanceTests.csproj +++ b/src/perf_tests/Python.PerformanceTests.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + compile @@ -25,7 +25,7 @@ - + diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index 2f4055fed..a77e40b7a 100644 --- a/src/runtime/Properties/AssemblyInfo.cs +++ b/src/runtime/Properties/AssemblyInfo.cs @@ -4,5 +4,5 @@ [assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] [assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] -[assembly: AssemblyVersion("2.0.26")] -[assembly: AssemblyFileVersion("2.0.26")] +[assembly: AssemblyVersion("2.0.27")] +[assembly: AssemblyFileVersion("2.0.27")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 0463bb748..25f01f3cb 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -5,7 +5,7 @@ Python.Runtime Python.Runtime QuantConnect.pythonnet - 2.0.26 + 2.0.27 false LICENSE https://github.com/pythonnet/pythonnet