| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -279,6 +279,134 @@ public void ConvertDateTimeWithExplicitUTCTimeZonePythonToCSharp() | |||
| 279 | 279 | } | |
| 280 | 280 | } | |
| 281 | 281 | ||
| 282 | + // The datetime instances produced for System.DateTime values coerce operations | ||
| 283 | + // against pure datetime.date operands using their date part instead of raising | ||
| 284 | + // TypeError, while behaving exactly like plain datetimes everywhere else. | ||
| 285 | + private static PyModule GetDateTimeCoercionModule() | ||
| 286 | + { | ||
| 287 | + return PyModule.FromString("datetime_coercion_test", @" | ||
| 288 | + from datetime import date, datetime, timedelta | ||
| 289 | + import pickle | ||
| 290 | + | ||
| 291 | + TODAY = date(2019, 7, 1) | ||
| 292 | + | ||
| 293 | + def dte(dt): | ||
| 294 | + return (dt - TODAY).days | ||
| 295 | + | ||
| 296 | + def reverse_dte(dt): | ||
| 297 | + return (TODAY - dt).days | ||
| 298 | + | ||
| 299 | + def compare_with_dates(dt): | ||
| 300 | + earlier = date(2019, 7, 1) | ||
| 301 | + later = date(2019, 12, 31) | ||
| 302 | + return [earlier < dt, earlier <= dt, dt > earlier, dt >= earlier, | ||
| 303 | + dt < later, dt <= later, later > dt, later >= dt] | ||
| 304 | + | ||
| 305 | + def same_day_comparisons(dt): | ||
| 306 | + same = date(dt.year, dt.month, dt.day) | ||
| 307 | + return [dt <= same, dt >= same, dt < same, dt > same, dt == same] | ||
| 308 | + | ||
| 309 | + def datetime_behavior_unchanged(dt): | ||
| 310 | + plain = datetime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond) | ||
| 311 | + shifted = dt + timedelta(days=1) | ||
| 312 | + return [isinstance(dt, datetime), dt == plain, hash(dt) == hash(plain), | ||
| 313 | + dt - plain == timedelta(0), shifted - dt == timedelta(days=1), | ||
| 314 | + dt < shifted, str(dt) == str(plain), repr(dt) == repr(plain), | ||
| 315 | + dt.strftime('%Y-%m-%d %H:%M') == plain.strftime('%Y-%m-%d %H:%M')] | ||
| 316 | + | ||
| 317 | + def pickle_as_plain_datetime(dt): | ||
| 318 | + restored = pickle.loads(pickle.dumps(dt)) | ||
| 319 | + plain = datetime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond) | ||
| 320 | + return [type(restored) is datetime, restored == plain] | ||
| 321 | + "); | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + [Test] | ||
| 325 | + public void ConvertedDateTimeSubtractionWithPureDateUsesDatePart() | ||
| 326 | + { | ||
| 327 | + using (Py.GIL()) | ||
| 328 | + { | ||
| 329 | + using var module = GetDateTimeCoercionModule(); | ||
| 330 | + // e.g. contract expiry minus the user's date.today() | ||
| 331 | + using var pyExpiry = Converter.ToPython(new DateTime(2019, 8, 15, 10, 30, 0)).MoveToPyObject(); | ||
| 332 | + | ||
| 333 | + using var dte = module.InvokeMethod("dte", pyExpiry); | ||
| 334 | + Assert.AreEqual(45, dte.As<int>()); | ||
| 335 | + | ||
| 336 | + using var reverseDte = module.InvokeMethod("reverse_dte", pyExpiry); | ||
| 337 | + Assert.AreEqual(-45, reverseDte.As<int>()); | ||
| 338 | + } | ||
| 339 | + } | ||
| 340 | + | ||
| 341 | + [Test] | ||
| 342 | + public void ConvertedDateTimeComparisonWithPureDateUsesDatePart() | ||
| 343 | + { | ||
| 344 | + using (Py.GIL()) | ||
| 345 | + { | ||
| 346 | + using var module = GetDateTimeCoercionModule(); | ||
| 347 | + using var pyDateTime = Converter.ToPython(new DateTime(2019, 8, 15, 10, 30, 0)).MoveToPyObject(); | ||
| 348 | + | ||
| 349 | + using var comparisons = module.InvokeMethod("compare_with_dates", pyDateTime); | ||
| 350 | + var results = comparisons.As<bool[]>(); | ||
| 351 | + for (var i = 0; i < results.Length; i++) | ||
| 352 | + { | ||
| 353 | + Assert.IsTrue(results[i], $"comparison {i} was false"); | ||
| 354 | + } | ||
| 355 | + } | ||
| 356 | + } | ||
| 357 | + | ||
| 358 | + [Test] | ||
| 359 | + public void ConvertedDateTimeSameDayComparisonWithPureDate() | ||
| 360 | + { | ||
| 361 | + using (Py.GIL()) | ||
| 362 | + { | ||
| 363 | + using var module = GetDateTimeCoercionModule(); | ||
| 364 | + using var pyDateTime = Converter.ToPython(new DateTime(2019, 8, 15, 10, 30, 0)).MoveToPyObject(); | ||
| 365 | + | ||
| 366 | + using var comparisons = module.InvokeMethod("same_day_comparisons", pyDateTime); | ||
| 367 | + var results = comparisons.As<bool[]>(); | ||
| 368 | + Assert.IsTrue(results[0], "dt <= same-day date"); | ||
| 369 | + Assert.IsTrue(results[1], "dt >= same-day date"); | ||
| 370 | + Assert.IsFalse(results[2], "dt < same-day date"); | ||
| 371 | + Assert.IsFalse(results[3], "dt > same-day date"); | ||
| 372 | + // equality with a pure date stays False: making it true would break the | ||
| 373 | + // hash contract since hash(datetime) != hash(date) | ||
| 374 | + Assert.IsFalse(results[4], "dt == same-day date"); | ||
| 375 | + } | ||
| 376 | + } | ||
| 377 | + | ||
| 378 | + [Test] | ||
| 379 | + public void ConvertedDateTimeBehavesLikePlainDateTime() | ||
| 380 | + { | ||
| 381 | + using (Py.GIL()) | ||
| 382 | + { | ||
| 383 | + using var module = GetDateTimeCoercionModule(); | ||
| 384 | + using var pyDateTime = Converter.ToPython(new DateTime(2019, 8, 15, 10, 30, 0, 5)).MoveToPyObject(); | ||
| 385 | + | ||
| 386 | + using var checks = module.InvokeMethod("datetime_behavior_unchanged", pyDateTime); | ||
| 387 | + var results = checks.As<bool[]>(); | ||
| 388 | + for (var i = 0; i < results.Length; i++) | ||
| 389 | + { | ||
| 390 | + Assert.IsTrue(results[i], $"behavior check {i} failed"); | ||
| 391 | + } | ||
| 392 | + } | ||
| 393 | + } | ||
| 394 | + | ||
| 395 | + [Test] | ||
| 396 | + public void ConvertedDateTimePicklesAsPlainDateTime() | ||
| 397 | + { | ||
| 398 | + using (Py.GIL()) | ||
| 399 | + { | ||
| 400 | + using var module = GetDateTimeCoercionModule(); | ||
| 401 | + using var pyDateTime = Converter.ToPython(new DateTime(2019, 8, 15, 10, 30, 0)).MoveToPyObject(); | ||
| 402 | + | ||
| 403 | + using var checks = module.InvokeMethod("pickle_as_plain_datetime", pyDateTime); | ||
| 404 | + var results = checks.As<bool[]>(); | ||
| 405 | + Assert.IsTrue(results[0], "unpickled type should be plain datetime.datetime"); | ||
| 406 | + Assert.IsTrue(results[1], "unpickled value should equal the original"); | ||
| 407 | + } | ||
| 408 | + } | ||
| 409 | + | ||
| 282 | 410 | [Test] | |
| 283 | 411 | public void ConvertTimestampRoundTrip() | |
| 284 | 412 | { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,7 +13,7 @@ | |||
| 13 | 13 | <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | |
| 14 | 14 | </PackageReference> | |
| 15 | 15 | <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.*" /> | |
| 16 | - <PackageReference Include="quantconnect.pythonnet" Version="2.0.64" GeneratePathProperty="true"> | ||
| 16 | + <PackageReference Include="quantconnect.pythonnet" Version="2.0.65" GeneratePathProperty="true"> | ||
| 17 | 17 | <IncludeAssets>compile</IncludeAssets> | |
| 18 | 18 | </PackageReference> | |
| 19 | 19 | </ItemGroup> | |
@@ -25,7 +25,7 @@ | |||
| 25 | 25 | </Target> | |
| 26 | 26 | ||
| 27 | 27 | <Target Name="CopyBaseline" AfterTargets="Build"> | |
| 28 | - <Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.64\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" /> | ||
| 28 | + <Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.65\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" /> | ||
| 29 | 29 | </Target> | |
| 30 | 30 | ||
| 31 | 31 | <Target Name="CopyNewBuild" AfterTargets="Build"> | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -58,7 +58,7 @@ internal static void Reset() | |||
| 58 | 58 | private static Type flagsType; | |
| 59 | 59 | private static Type boolType; | |
| 60 | 60 | private static Type typeType; | |
| 61 | - private static PyObject dateTimeCtor; | ||
| 61 | + private static Lazy<PyObject> dateTimeCtor; | ||
| 62 | 62 | private static PyObject timeSpanCtor; | |
| 63 | 63 | private static Lazy<PyObject> tzInfoCtor; | |
| 64 | 64 | private static PyObject pyTupleNoKind; | |
@@ -94,8 +94,62 @@ static Converter() | |||
| 94 | 94 | var dateTimeMod = Runtime.PyImport_ImportModule("datetime"); | |
| 95 | 95 | PythonException.ThrowIfIsNull(dateTimeMod); | |
| 96 | 96 | ||
| 97 | - dateTimeCtor = Runtime.PyObject_GetAttrString(dateTimeMod.Borrow(), "datetime").MoveToPyObject(); | ||
| 98 | - PythonException.ThrowIfIsNull(dateTimeCtor); | ||
| 97 | + dateTimeCtor = new Lazy<PyObject>(() => | ||
| 98 | + { | ||
| 99 | + // datetime.datetime subclass whose subtraction and ordering against pure | ||
| 100 | + // datetime.date operands coerce to the date part instead of raising TypeError. | ||
| 101 | + // Equality and hashing are left untouched: making a datetime equal a date | ||
| 102 | + // would break the hash contract. Pickling degrades to the plain datetime | ||
| 103 | + // class so payloads never reference this synthetic module. | ||
| 104 | + var dateTimeSubclassMod = PyModule.FromString("clr_datetime", @" | ||
| 105 | + from datetime import datetime as _datetime, date as _date | ||
| 106 | + | ||
| 107 | + class datetime(_datetime): | ||
| 108 | + __slots__ = () | ||
| 109 | + | ||
| 110 | + def __sub__(self, other): | ||
| 111 | + if isinstance(other, _date) and not isinstance(other, _datetime): | ||
| 112 | + return self.date() - other | ||
| 113 | + return _datetime.__sub__(self, other) | ||
| 114 | + | ||
| 115 | + def __rsub__(self, other): | ||
| 116 | + if isinstance(other, _date) and not isinstance(other, _datetime): | ||
| 117 | + return other - self.date() | ||
| 118 | + return _datetime.__rsub__(self, other) | ||
| 119 | + | ||
| 120 | + def __lt__(self, other): | ||
| 121 | + if isinstance(other, _date) and not isinstance(other, _datetime): | ||
| 122 | + return self.date() < other | ||
| 123 | + return _datetime.__lt__(self, other) | ||
| 124 | + | ||
| 125 | + def __le__(self, other): | ||
| 126 | + if isinstance(other, _date) and not isinstance(other, _datetime): | ||
| 127 | + return self.date() <= other | ||
| 128 | + return _datetime.__le__(self, other) | ||
| 129 | + | ||
| 130 | + def __gt__(self, other): | ||
| 131 | + if isinstance(other, _date) and not isinstance(other, _datetime): | ||
| 132 | + return self.date() > other | ||
| 133 | + return _datetime.__gt__(self, other) | ||
| 134 | + | ||
| 135 | + def __ge__(self, other): | ||
| 136 | + if isinstance(other, _date) and not isinstance(other, _datetime): | ||
| 137 | + return self.date() >= other | ||
| 138 | + return _datetime.__ge__(self, other) | ||
| 139 | + | ||
| 140 | + def __repr__(self): | ||
| 141 | + base = _datetime.__repr__(self) | ||
| 142 | + return 'datetime.datetime' + base[base.index('('):] | ||
| 143 | + | ||
| 144 | + def __reduce_ex__(self, protocol): | ||
| 145 | + return (_datetime, (self.year, self.month, self.day, self.hour, self.minute, | ||
| 146 | + self.second, self.microsecond, self.tzinfo)) | ||
| 147 | + ").BorrowNullable(); | ||
| 148 | + | ||
| 149 | + var result = Runtime.PyObject_GetAttrString(dateTimeSubclassMod, "datetime").MoveToPyObject(); | ||
| 150 | + PythonException.ThrowIfIsNull(result); | ||
| 151 | + return result; | ||
| 152 | + }); | ||
| 99 | 153 | ||
| 100 | 154 | timeSpanCtor = Runtime.PyObject_GetAttrString(dateTimeMod.Borrow(), "timedelta").MoveToPyObject(); | |
| 101 | 155 | PythonException.ThrowIfIsNull(timeSpanCtor); | |
@@ -375,7 +429,7 @@ internal static NewReference ToPython(object? value, Type type) | |||
| 375 | 429 | Runtime.PyTuple_SetItem(dateTimeArgs, 7, TzInfo(datetime.Kind).Steal()); | |
| 376 | 430 | } | |
| 377 | 431 | ||
| 378 | - var returnDateTime = Runtime.PyObject_CallObject(dateTimeCtor, dateTimeArgs); | ||
| 432 | + var returnDateTime = Runtime.PyObject_CallObject(dateTimeCtor.Value, dateTimeArgs); | ||
| 379 | 433 | return returnDateTime; | |
| 380 | 434 | ||
| 381 | 435 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,5 +4,5 @@ | |||
| 4 | 4 | [assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] | |
| 5 | 5 | [assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] | |
| 6 | 6 | ||
| 7 | - [assembly: AssemblyVersion("2.0.64")] | ||
| 8 | - [assembly: AssemblyFileVersion("2.0.64")] | ||
| 7 | + [assembly: AssemblyVersion("2.0.65")] | ||
| 8 | + [assembly: AssemblyFileVersion("2.0.65")] | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,7 +5,7 @@ | |||
| 5 | 5 | <RootNamespace>Python.Runtime</RootNamespace> | |
| 6 | 6 | <AssemblyName>Python.Runtime</AssemblyName> | |
| 7 | 7 | <PackageId>QuantConnect.pythonnet</PackageId> | |
| 8 | - <Version>2.0.64</Version> | ||
| 8 | + <Version>2.0.65</Version> | ||
| 9 | 9 | <GenerateAssemblyInfo>false</GenerateAssemblyInfo> | |
| 10 | 10 | <PackageLicenseFile>LICENSE</PackageLicenseFile> | |
| 11 | 11 | <RepositoryUrl>https://github.com/pythonnet/pythonnet</RepositoryUrl> | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -544,7 +544,9 @@ def test_datetime_conversion(): | |||
| 544 | 544 | from datetime import datetime | |
| 545 | 545 | ||
| 546 | 546 | ob = ConversionTest() | |
| 547 | - assert type(ob.DateTimeField) is type(datetime(1,1,1)) | ||
| 547 | + # System.DateTime converts to a datetime subclass that also supports | ||
| 548 | + # arithmetic and ordering against pure datetime.date operands | ||
| 549 | + assert isinstance(ob.DateTimeField, datetime) | ||
| 548 | 550 | assert ob.DateTimeField.day == 1 | |
| 549 | 551 | ||
| 550 | 552 | ob.DateTimeField = datetime(2000,1,2) | |
@@ -558,6 +560,32 @@ def test_datetime_conversion(): | |||
| 558 | 560 | with pytest.raises(TypeError): | |
| 559 | 561 | ConversionTest().DateTimeField = "spam" | |
| 560 | 562 | ||
| 563 | + def test_datetime_date_coercion(): | ||
| 564 | + """Converted System.DateTime values coerce operations against pure | ||
| 565 | + datetime.date operands using their date part instead of raising TypeError.""" | ||
| 566 | + from datetime import date, datetime, timedelta | ||
| 567 | + | ||
| 568 | + ob = ConversionTest() | ||
| 569 | + ob.DateTimeField = datetime(2019, 8, 15, 10, 30, 0) | ||
| 570 | + value = ob.DateTimeField | ||
| 571 | + | ||
| 572 | + assert (value - date(2019, 7, 1)).days == 45 | ||
| 573 | + assert (date(2019, 7, 1) - value).days == -45 | ||
| 574 | + assert value > date(2019, 7, 1) | ||
| 575 | + assert date(2019, 7, 1) <= value | ||
| 576 | + assert value <= date(2019, 8, 15) | ||
| 577 | + assert value >= date(2019, 8, 15) | ||
| 578 | + # equality with a pure date stays False (hash contract preserved) | ||
| 579 | + assert not value == date(2019, 8, 15) | ||
| 580 | + | ||
| 581 | + # plain datetime behavior is unchanged | ||
| 582 | + assert value == datetime(2019, 8, 15, 10, 30, 0) | ||
| 583 | + assert hash(value) == hash(datetime(2019, 8, 15, 10, 30, 0)) | ||
| 584 | + assert value - datetime(2019, 8, 15) == timedelta(hours=10, minutes=30) | ||
| 585 | + assert value + timedelta(days=1) == datetime(2019, 8, 16, 10, 30, 0) | ||
| 586 | + assert repr(value) == repr(datetime(2019, 8, 15, 10, 30)) | ||
| 587 | + assert str(value) == str(datetime(2019, 8, 15, 10, 30)) | ||
| 588 | + | ||
| 561 | 589 | def test_string_conversion(): | |
| 562 | 590 | """Test string / unicode conversion.""" | |
| 563 | 591 | ob = ConversionTest() | |
| Back | FazBrowse Home | New Git URL |
0 commit comments