Dubious type casting for augmented arithmetic operators
We currently allow, and validate via testing in test_Data_BINARY_AND_UNARY_OPERATORS, some behaviour relating to input and output data types for augmented arithmetic assignment operators that is not allowed by NumPy, and we should consider whether this is suitable or not. I am inclined to say we should make appropriate changes to adopt the NumPy behaviour.
Specifics
Namely, when an augmented assignment is performed using inputs with data types which lead to a change in data type for the output, e.g. for a simplified scalar case something like a = 1; a += 1.0, we permit an in-place change of array dtype. As a minimal example, note how NumPy raises a type casting error for the equivalent operation below, whereas we go ahead and produce an output with a changed data type, the same type that the operation not in-place would produce:
>>> import cf >>> import numpy as np >>> >>> # Setup equivalent arrays >>> i_np = np.array([1, 2, 3]) >>> i_cf = cf.Data(i_np) >>> >>> # NumPy raises a type casting error: >>> i_np + 1.0 # operation not in-place is fine array([2., 3., 4.]) >>> i_np += 1.0 Traceback (most recent call last): File "<stdin>", line 1, in <module> numpy.core._exceptions.UFuncTypeError: Cannot cast ufunc 'add' output from dtype('float64') to dtype('int64') with casting rule 'same_kind' >>> >>> # ... whereas cf performs the operation to give the same result data type >>> # as the non in-place operation would: >>> i_cf + 1.0 <CF Data(3): [2.0, 3.0, 4.0]> >>> i_cf += 1.0 >>> i_cf <CF Data(3): [2.0, 3.0, 4.0]>
and the equivalent behaviour occurs for the various __i<operator>__ operators.
Relevant cases in test suite
For reference, the tests in test_Data_BINARY_AND_UNARY_OPERATORS which were checking for this (dubious) behaviour, which remain as such from before the LAMA to Dask migration, are:
| a = a0.copy() | |
| try: | |
| a += x | |
| except TypeError: | |
| pass | |
| else: | |
| e = d.copy() | |
| e += x | |
| message = "Failed in {!r}+={}".format(d, x) | |
| self.assertTrue( | |
| e.equals(cf.Data(a, "m"), verbose=1), message | |
| ) | |
| a = a0.copy() | |
| try: | |
| a *= x | |
| except TypeError: | |
| pass | |
| else: | |
| e = d.copy() | |
| e *= x | |
| message = "Failed in {!r}*={}".format(d, x) | |
| self.assertTrue( | |
| e.equals(cf.Data(a, "m"), verbose=1), message | |
| ) | |
| a = a0.copy() | |
| try: | |
| a /= x | |
| except TypeError: | |
| pass | |
| else: | |
| e = d.copy() | |
| e /= x | |
| message = "Failed in {!r}/={}".format(d, x) | |
| self.assertTrue( | |
| e.equals(cf.Data(a, "m"), verbose=1), message | |
| ) | |
| a = a0.copy() | |
| try: | |
| a -= x | |
| except TypeError: | |
| pass | |
| else: | |
| e = d.copy() | |
| e -= x | |
| message = "Failed in {!r}-={}".format(d, x) | |
| self.assertTrue( | |
| e.equals(cf.Data(a, "m"), verbose=1), message | |
| ) | |
| a = a0.copy() | |
| try: | |
| a //= x | |
| except TypeError: | |
| pass | |
| else: | |
| e = d.copy() | |
| e //= x | |
| message = "Failed in {!r}//={}".format(d, x) | |
| self.assertTrue( | |
| e.equals(cf.Data(a, "m"), verbose=1), message | |
| ) | |
| # TODODASK SB: re-instate this once _combined_units is sorted, | |
| # presently fails with error, as with __pow__: | |
| # AttributeError: 'Data' object has no attribute '_size' | |
| # a = a0.copy() | |
| # try: | |
| # a **= x | |
| # except TypeError: | |
| # pass | |
| # else: | |
| # e = d.copy() | |
| # e **= x | |
| # message = "Failed in {!r}**={}".format(d, x) | |
| # self.assertTrue( | |
| # e.equals(cf.Data(a, "m2"), verbose=1), message | |
| # ) | |
| a = a0.copy() | |
| try: | |
| a.__itruediv__(x) | |
| except TypeError: | |
| pass | |
| else: | |
| e = d.copy() | |
| e.__itruediv__(x) | |
| message = "Failed in {!r}.__itruediv__({})".format(d, x) | |
| self.assertTrue( | |
| e.equals(cf.Data(a, "m"), verbose=1), message | |
| ) |