|
| 1 | +using System; |
1 | 2 | using NUnit.Framework; |
2 | 3 | using Python.Runtime; |
3 | 4 |
|
@@ -42,6 +43,53 @@ public ConvertibleMoney(decimal amount) |
42 | 43 | public static ConvertibleMoney operator +(ConvertibleMoney a, ConvertibleMoney b) => new(a.Amount + b.Amount); |
43 | 44 | } |
44 | 45 |
|
| 46 | + public class ConvertibleCount |
| 47 | + { |
| 48 | + public long Value { get; } |
| 49 | + |
| 50 | + public ConvertibleCount(long value) |
| 51 | + { |
| 52 | + Value = value; |
| 53 | + } |
| 54 | + |
| 55 | + public static implicit operator long(ConvertibleCount count) => count.Value; |
| 56 | + } |
| 57 | + |
| 58 | + public class ConvertibleBase |
| 59 | + { |
| 60 | + // inherited by ConvertibleDerived, it must keep winning over the synthesized one as in C# |
| 61 | + public static string operator +(ConvertibleBase a, ConvertibleBase b) => "base"; |
| 62 | + } |
| 63 | + |
| 64 | + public class ConvertibleDerived : ConvertibleBase |
| 65 | + { |
| 66 | + public decimal Amount { get; } |
| 67 | + |
| 68 | + public ConvertibleDerived(decimal amount) |
| 69 | + { |
| 70 | + Amount = amount; |
| 71 | + } |
| 72 | + |
| 73 | + public static implicit operator decimal(ConvertibleDerived derived) => derived.Amount; |
| 74 | + } |
| 75 | + |
| 76 | + public class PreciseQuantity : IComparable<PreciseQuantity> |
| 77 | + { |
| 78 | + public decimal Value { get; } |
| 79 | + |
| 80 | + public PreciseQuantity(decimal value) |
| 81 | + { |
| 82 | + Value = value; |
| 83 | + } |
| 84 | + |
| 85 | + // equal to 1 once converted to double, only the type's own ordering tells them apart |
| 86 | + public static PreciseQuantity AboveOne() => new(1.0000000000000000000000000001m); |
| 87 | + |
| 88 | + public int CompareTo(PreciseQuantity other) => Value.CompareTo(other.Value); |
| 89 | + |
| 90 | + public static implicit operator decimal(PreciseQuantity quantity) => quantity.Value; |
| 91 | + } |
| 92 | + |
45 | 93 | public class NotConvertible |
46 | 94 | { |
47 | 95 | } |
@@ -79,6 +127,62 @@ def own_operator_wins(): |
79 | 127 |
|
80 | 128 | def not_convertible(): |
81 | 129 | return hasattr(NotConvertible(), '__add__'), hasattr(NotConvertible(), '__float__') |
| 130 | +
|
| 131 | +count = ConvertibleCount(2) |
| 132 | +big = ConvertibleCount(2**53 + 1) |
| 133 | +
|
| 134 | +def integer_arithmetic(): |
| 135 | + results = (count + 1, 1 + count, count + count, count - 5, 10 - count, count * 3, 3 * count, -count, count + True) |
| 136 | + return all(type(result) is int for result in results), results |
| 137 | +
|
| 138 | +def integer_with_floats(): |
| 139 | + results = (count + 0.5, 0.5 + count, count * 1.5, count / 2, count / count, 3 / count) |
| 140 | + return all(type(result) is float for result in results), results |
| 141 | +
|
| 142 | +def large_integers(): |
| 143 | + exact = 2**53 + 1 |
| 144 | + return (int(big) == exact, big > 2**53, big - 1 == 2**53, big * big == exact * exact, |
| 145 | + -big == -exact, big >= exact, big + 2**70 == exact + 2**70) |
| 146 | +
|
| 147 | +def int_conversions(): |
| 148 | + errors = [] |
| 149 | + for value in (float('nan'), float('inf')): |
| 150 | + try: |
| 151 | + int(ConvertibleRatio(value)) |
| 152 | + except Exception as e: |
| 153 | + errors.append(type(e).__name__) |
| 154 | + return errors, int(ConvertibleRatio(-1.75)), int(ConvertibleFee(12345678901234567891)) == 12345678901234567891 |
| 155 | +
|
| 156 | +def division_by_zero(): |
| 157 | + errors = [] |
| 158 | + for operation in (lambda: fee / 0, lambda: 1 / ConvertibleFee(0), lambda: count / 0.0, lambda: fee / ConvertibleFee(0)): |
| 159 | + try: |
| 160 | + operation() |
| 161 | + except ZeroDivisionError: |
| 162 | + errors.append(True) |
| 163 | + return len(errors) |
| 164 | +
|
| 165 | +def other_numeric_operands(): |
| 166 | + margin = ConvertibleRatio(0.5) |
| 167 | + return fee + 2**40, count + 2**70, fee + margin, count * margin, count < 2**64 |
| 168 | +
|
| 169 | +def unsupported_operands(): |
| 170 | + messages = [] |
| 171 | + for operation in (lambda: count + 'x', lambda: fee * None, lambda: fee < 'x', lambda: NotConvertible() - fee): |
| 172 | + try: |
| 173 | + operation() |
| 174 | + messages.append(None) |
| 175 | + except TypeError as e: |
| 176 | + messages.append(str(e)) |
| 177 | + return messages |
| 178 | +
|
| 179 | +def inherited_operator_wins(): |
| 180 | + derived = ConvertibleDerived(2) |
| 181 | + return derived + derived, derived - ConvertibleDerived(1), float(derived) |
| 182 | +
|
| 183 | +def own_ordering_wins(): |
| 184 | + one, above = PreciseQuantity(1), PreciseQuantity.AboveOne() |
| 185 | + return above > one, one < above, above >= one, one <= above, above < one, sorted([above, one])[0] is one |
82 | 186 | "; |
83 | 187 |
|
84 | 188 | [OneTimeSetUp] |
@@ -150,6 +254,107 @@ public void TheTypesOwnOperatorsKeepWinning() |
150 | 254 | Assert.AreEqual(1.0, (double)result[2]); |
151 | 255 | } |
152 | 256 |
|
| 257 | + [Test] |
| 258 | + public void IntegralConversionsBehaveLikeInts() |
| 259 | + { |
| 260 | + using var _ = Py.GIL(); |
| 261 | + var result = module.integer_arithmetic(); |
| 262 | + Assert.IsTrue((bool)result[0], "every result must be an int"); |
| 263 | + var expected = new long[] { 3, 3, 4, -3, 8, 6, 6, -2, 3 }; |
| 264 | + for (var i = 0; i < expected.Length; i++) |
| 265 | + { |
| 266 | + Assert.AreEqual(expected[i], (long)result[1][i], $"index {i}"); |
| 267 | + } |
| 268 | + } |
| 269 | + |
| 270 | + [Test] |
| 271 | + public void IntegralConversionsGiveFloatsWherePythonWould() |
| 272 | + { |
| 273 | + using var _ = Py.GIL(); |
| 274 | + var result = module.integer_with_floats(); |
| 275 | + Assert.IsTrue((bool)result[0], "every result must be a float"); |
| 276 | + var expected = new[] { 2.5, 2.5, 3.0, 1.0, 1.0, 1.5 }; |
| 277 | + for (var i = 0; i < expected.Length; i++) |
| 278 | + { |
| 279 | + Assert.AreEqual(expected[i], (double)result[1][i], $"index {i}"); |
| 280 | + } |
| 281 | + } |
| 282 | + |
| 283 | + [Test] |
| 284 | + public void LargeIntegralValuesStayExact() |
| 285 | + { |
| 286 | + using var _ = Py.GIL(); |
| 287 | + var result = module.large_integers(); |
| 288 | + for (var i = 0; i < 7; i++) |
| 289 | + { |
| 290 | + Assert.IsTrue((bool)result[i], $"index {i}"); |
| 291 | + } |
| 292 | + } |
| 293 | + |
| 294 | + [Test] |
| 295 | + public void IntConversionFollowsPython() |
| 296 | + { |
| 297 | + using var _ = Py.GIL(); |
| 298 | + var result = module.int_conversions(); |
| 299 | + CollectionAssert.AreEqual(new[] { "ValueError", "OverflowError" }, ((PyObject)result[0]).As<string[]>()); |
| 300 | + Assert.AreEqual(-1, (int)result[1]); |
| 301 | + Assert.IsTrue((bool)result[2], "decimal conversions must truncate exactly"); |
| 302 | + } |
| 303 | + |
| 304 | + [Test] |
| 305 | + public void DivisionByZeroRaisesLikePython() |
| 306 | + { |
| 307 | + using var _ = Py.GIL(); |
| 308 | + Assert.AreEqual(4, (int)module.division_by_zero()); |
| 309 | + } |
| 310 | + |
| 311 | + [Test] |
| 312 | + public void LargeIntsAndOtherNumbersAreOperands() |
| 313 | + { |
| 314 | + using var _ = Py.GIL(); |
| 315 | + var result = module.other_numeric_operands(); |
| 316 | + Assert.AreEqual(2.0 + Math.Pow(2, 40), (double)result[0]); |
| 317 | + Assert.AreEqual("1180591620717411303426", result[1].ToString()); |
| 318 | + // another type with an implicit numeric conversion is read through its __float__ |
| 319 | + Assert.AreEqual(2.5, (double)result[2]); |
| 320 | + Assert.AreEqual(1.0, (double)result[3]); |
| 321 | + Assert.IsTrue((bool)result[4]); |
| 322 | + } |
| 323 | + |
| 324 | + [Test] |
| 325 | + public void UnsupportedOperandsRaisePythonsTypeError() |
| 326 | + { |
| 327 | + using var _ = Py.GIL(); |
| 328 | + var messages = ((PyObject)module.unsupported_operands()).As<string[]>(); |
| 329 | + StringAssert.Contains("unsupported operand type(s) for +", messages[0]); |
| 330 | + StringAssert.Contains("unsupported operand type(s) for *", messages[1]); |
| 331 | + StringAssert.Contains("'<' not supported", messages[2]); |
| 332 | + StringAssert.Contains("unsupported operand type(s) for -", messages[3]); |
| 333 | + } |
| 334 | + |
| 335 | + [Test] |
| 336 | + public void InheritedOperatorsKeepWinning() |
| 337 | + { |
| 338 | + using var _ = Py.GIL(); |
| 339 | + var result = module.inherited_operator_wins(); |
| 340 | + Assert.AreEqual("base", (string)result[0]); |
| 341 | + // subtraction is declared nowhere in the hierarchy, so the synthesized one applies |
| 342 | + Assert.AreEqual(1.0, (double)result[1]); |
| 343 | + Assert.AreEqual(2.0, (double)result[2]); |
| 344 | + } |
| 345 | + |
| 346 | + [Test] |
| 347 | + public void TheTypesOwnOrderingWinsBetweenItsInstances() |
| 348 | + { |
| 349 | + using var _ = Py.GIL(); |
| 350 | + var result = module.own_ordering_wins(); |
| 351 | + var expected = new[] { true, true, true, true, false, true }; |
| 352 | + for (var i = 0; i < expected.Length; i++) |
| 353 | + { |
| 354 | + Assert.AreEqual(expected[i], (bool)result[i], $"index {i}"); |
| 355 | + } |
| 356 | + } |
| 357 | + |
153 | 358 | [Test] |
154 | 359 | public void TypesWithoutAConversionAreUntouched() |
155 | 360 | { |
|
0 commit comments