diff --git a/src/embed_tests/TestConverter.cs b/src/embed_tests/TestConverter.cs index 3d68456e3..40ed9ff48 100644 --- a/src/embed_tests/TestConverter.cs +++ b/src/embed_tests/TestConverter.cs @@ -231,6 +231,36 @@ def GetNextDay(dateTime): var expectedDateTime = new DateTime(year, month, day, hour, minute, second); Assert.AreEqual(expectedDateTime, managedDateTime); + + Assert.AreEqual(DateTimeKind.Unspecified, managedDateTime.Kind); + } + } + + [Test] + public void ConvertDateTimeWithExplicitUTCTimeZonePythonToCSharp() + { + 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()) + { + var csDateTime = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Utc); + // Converter.ToPython will set the datetime tzinfo to UTC using a custom tzinfo class + using var pyDateTime = Converter.ToPython(csDateTime).MoveToPyObject(); + 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(DateTimeKind.Utc, managedDateTime.Kind); } } diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 373383145..708d6572e 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/Converter.cs b/src/runtime/Converter.cs index 3f249f0aa..d42ff958a 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -1123,13 +1123,31 @@ 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 (!ReferenceNullOrNone(tzinfo)) + { + // We set the datetime kind to UTC if the tzinfo was set to UTC by the ToPthon method + // using it's custom GMT Python tzinfo class + hours = Runtime.PyObject_GetAttrString(tzinfo.Borrow(), hoursPtr); + minutes = Runtime.PyObject_GetAttrString(tzinfo.Borrow(), minutesPtr); + if (!ReferenceNullOrNone(hours) && + !ReferenceNullOrNone(minutes) && + Runtime.PyLong_AsLong(hours.Borrow()) == 0 && Runtime.PyLong_AsLong(minutes.Borrow()) == 0) + { + timeKind = DateTimeKind.Utc; + } + } var convertedHour = 0L; var convertedMinute = 0L; var convertedSecond = 0L; var milliseconds = 0L; // could be python date type - if (!hour.IsNull() && !hour.IsNone()) + if (!ReferenceNullOrNone(hour)) { convertedHour = Runtime.PyLong_AsLong(hour.Borrow()); convertedMinute = Runtime.PyLong_AsLong(minute.Borrow()); @@ -1143,7 +1161,8 @@ internal static bool ToPrimitive(BorrowedReference value, Type obType, out objec (int)convertedHour, (int)convertedMinute, (int)convertedSecond, - (int)milliseconds); + (int)milliseconds, + timeKind); year.Dispose(); month.Dispose(); @@ -1153,6 +1172,16 @@ 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: @@ -1183,6 +1212,11 @@ internal static bool ToPrimitive(BorrowedReference value, Type obType, out objec return false; } + private static bool ReferenceNullOrNone(NewReference reference) + { + return reference.IsNull() || reference.IsNone(); + } + private static void SetConversionError(BorrowedReference value, Type target) { diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index a77e40b7a..5eaf718eb 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.27")] -[assembly: AssemblyFileVersion("2.0.27")] +[assembly: AssemblyVersion("2.0.28")] +[assembly: AssemblyFileVersion("2.0.28")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 25f01f3cb..4677ea191 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.27 + 2.0.28 false LICENSE https://github.com/pythonnet/pythonnet