|
| 1 | +using System; |
| 2 | +using NUnit.Framework; |
| 3 | +using Python.Runtime; |
| 4 | + |
| 5 | +namespace Python.EmbeddingTest |
| 6 | +{ |
| 7 | + public class ConvertibleFee |
| 8 | + { |
| 9 | + public decimal Amount { get; } |
| 10 | + |
| 11 | + public ConvertibleFee(decimal amount) |
| 12 | + { |
| 13 | + Amount = amount; |
| 14 | + } |
| 15 | + |
| 16 | + public static implicit operator decimal(ConvertibleFee fee) => fee.Amount; |
| 17 | + } |
| 18 | + |
| 19 | + public class ConvertibleRatio |
| 20 | + { |
| 21 | + public double Value { get; } |
| 22 | + |
| 23 | + public ConvertibleRatio(double value) |
| 24 | + { |
| 25 | + Value = value; |
| 26 | + } |
| 27 | + |
| 28 | + public static implicit operator double(ConvertibleRatio ratio) => ratio.Value; |
| 29 | + } |
| 30 | + |
| 31 | + public class ConvertibleMoney |
| 32 | + { |
| 33 | + public decimal Amount { get; } |
| 34 | + |
| 35 | + public ConvertibleMoney(decimal amount) |
| 36 | + { |
| 37 | + Amount = amount; |
| 38 | + } |
| 39 | + |
| 40 | + public static implicit operator decimal(ConvertibleMoney money) => money.Amount; |
| 41 | + |
| 42 | + // the type's own operator must keep winning over the synthesized one |
| 43 | + public static ConvertibleMoney operator +(ConvertibleMoney a, ConvertibleMoney b) => new(a.Amount + b.Amount); |
| 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 | + |
| 93 | + public class NotConvertible |
| 94 | + { |
| 95 | + } |
| 96 | + |
| 97 | + public class TestImplicitNumericOperators |
| 98 | + { |
| 99 | + private static dynamic module; |
| 100 | + |
| 101 | + private const string TestModule = @" |
| 102 | +from clr import AddReference |
| 103 | +AddReference(""System"") |
| 104 | +AddReference(""Python.EmbeddingTest"") |
| 105 | +from Python.EmbeddingTest import * |
| 106 | +
|
| 107 | +fee = ConvertibleFee(2) |
| 108 | +other = ConvertibleFee(3) |
| 109 | +
|
| 110 | +def arithmetic(): |
| 111 | + return (fee + other, fee + 1, 1 + fee, fee - other, fee - 1, 10 - fee, |
| 112 | + fee * other, fee * 2, 2 * fee, other / fee, fee / 2, 10 / fee, -fee) |
| 113 | +
|
| 114 | +def aggregates(): |
| 115 | + return sum([fee, other]), max(fee, other) is other, sorted([other, fee])[0] is fee |
| 116 | +
|
| 117 | +def comparisons(): |
| 118 | + return (fee < other, fee > other, fee <= other, fee >= other, |
| 119 | + fee > 0, fee < 2, fee <= 2, fee >= 2, 1 < fee, 2 >= fee) |
| 120 | +
|
| 121 | +def conversions(): |
| 122 | + return float(fee), int(ConvertibleRatio(1.75)), float(ConvertibleRatio(1.75)) |
| 123 | +
|
| 124 | +def own_operator_wins(): |
| 125 | + total = ConvertibleMoney(2) + ConvertibleMoney(3) |
| 126 | + return type(total).__name__, total.Amount, ConvertibleMoney(2) - ConvertibleMoney(1) |
| 127 | +
|
| 128 | +def not_convertible(): |
| 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 |
| 186 | +"; |
| 187 | + |
| 188 | + [OneTimeSetUp] |
| 189 | + public void OneTimeSetUp() |
| 190 | + { |
| 191 | + PythonEngine.Initialize(); |
| 192 | + using var _ = Py.GIL(); |
| 193 | + module = PyModule.FromString("implicit_numeric_operators", TestModule); |
| 194 | + } |
| 195 | + |
| 196 | + [OneTimeTearDown] |
| 197 | + public void Dispose() |
| 198 | + { |
| 199 | + PythonEngine.Shutdown(); |
| 200 | + } |
| 201 | + |
| 202 | + [Test] |
| 203 | + public void ArithmeticDelegatesToTheImplicitConversion() |
| 204 | + { |
| 205 | + using var _ = Py.GIL(); |
| 206 | + var result = module.arithmetic(); |
| 207 | + var expected = new[] { 5.0, 3.0, 3.0, -1.0, 1.0, 8.0, 6.0, 4.0, 4.0, 1.5, 1.0, 5.0, -2.0 }; |
| 208 | + for (var i = 0; i < expected.Length; i++) |
| 209 | + { |
| 210 | + Assert.AreEqual(expected[i], (double)result[i], $"index {i}"); |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + [Test] |
| 215 | + public void AggregatesWork() |
| 216 | + { |
| 217 | + using var _ = Py.GIL(); |
| 218 | + var result = module.aggregates(); |
| 219 | + Assert.AreEqual(5.0, (double)result[0]); |
| 220 | + Assert.IsTrue((bool)result[1]); |
| 221 | + Assert.IsTrue((bool)result[2]); |
| 222 | + } |
| 223 | + |
| 224 | + [Test] |
| 225 | + public void ComparisonsDelegateToTheImplicitConversion() |
| 226 | + { |
| 227 | + using var _ = Py.GIL(); |
| 228 | + var result = module.comparisons(); |
| 229 | + var expected = new[] { true, false, true, false, true, false, true, true, true, true }; |
| 230 | + for (var i = 0; i < expected.Length; i++) |
| 231 | + { |
| 232 | + Assert.AreEqual(expected[i], (bool)result[i], $"index {i}"); |
| 233 | + } |
| 234 | + } |
| 235 | + |
| 236 | + [Test] |
| 237 | + public void FloatAndIntConversions() |
| 238 | + { |
| 239 | + using var _ = Py.GIL(); |
| 240 | + var result = module.conversions(); |
| 241 | + Assert.AreEqual(2.0, (double)result[0]); |
| 242 | + Assert.AreEqual(1, (int)result[1]); |
| 243 | + Assert.AreEqual(1.75, (double)result[2]); |
| 244 | + } |
| 245 | + |
| 246 | + [Test] |
| 247 | + public void TheTypesOwnOperatorsKeepWinning() |
| 248 | + { |
| 249 | + using var _ = Py.GIL(); |
| 250 | + var result = module.own_operator_wins(); |
| 251 | + Assert.AreEqual("ConvertibleMoney", (string)result[0]); |
| 252 | + Assert.AreEqual(5m, (decimal)result[1]); |
| 253 | + // subtraction is not declared by the type, so the synthesized one applies |
| 254 | + Assert.AreEqual(1.0, (double)result[2]); |
| 255 | + } |
| 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 | + |
| 358 | + [Test] |
| 359 | + public void TypesWithoutAConversionAreUntouched() |
| 360 | + { |
| 361 | + using var _ = Py.GIL(); |
| 362 | + var result = module.not_convertible(); |
| 363 | + Assert.IsFalse((bool)result[0]); |
| 364 | + Assert.IsFalse((bool)result[1]); |
| 365 | + } |
| 366 | + } |
| 367 | +} |
0 commit comments