diff --git a/PythonClient/airsim/tests/__init__.py b/PythonClient/airsim/tests/__init__.py new file mode 100644 index 0000000000..7005bb9b82 --- /dev/null +++ b/PythonClient/airsim/tests/__init__.py @@ -0,0 +1 @@ +# Unit tests for airsim Python package (no simulator required). diff --git a/PythonClient/airsim/tests/test_types_numpy2_scalars.py b/PythonClient/airsim/tests/test_types_numpy2_scalars.py new file mode 100644 index 0000000000..8828240eda --- /dev/null +++ b/PythonClient/airsim/tests/test_types_numpy2_scalars.py @@ -0,0 +1,64 @@ +"""Vector3r/Quaternionr scalar ops must work under NumPy 2 (no np.sctypes).""" +import importlib.util +import os +import sys +import unittest + +import numpy as np + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_TYPES = os.path.join(os.path.dirname(_HERE), "types.py") + + +def _load_types(): + spec = importlib.util.spec_from_file_location("airsim_types_under_test", _TYPES) + mod = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = mod + spec.loader.exec_module(mod) + return mod + + +_types = _load_types() +Vector3r = _types.Vector3r +Quaternionr = _types.Quaternionr + + +class TestNumpy2Scalars(unittest.TestCase): + def test_vector3_mul_python_float(self): + v = Vector3r(1.0, 2.0, 3.0) * 2.0 + self.assertEqual((v.x_val, v.y_val, v.z_val), (2.0, 4.0, 6.0)) + + def test_vector3_mul_numpy_float32(self): + v = Vector3r(1.0, -1.0, 0.5) * np.float32(2.0) + self.assertAlmostEqual(float(v.x_val), 2.0, places=5) + self.assertAlmostEqual(float(v.y_val), -2.0, places=5) + self.assertAlmostEqual(float(v.z_val), 1.0, places=5) + + def test_vector3_div_numpy_int64(self): + v = Vector3r(2.0, 4.0, 6.0) / np.int64(2) + self.assertEqual( + (float(v.x_val), float(v.y_val), float(v.z_val)), + (1.0, 2.0, 3.0), + ) + + def test_vector3_mul_rejects_str(self): + with self.assertRaises(TypeError): + _ = Vector3r(1, 2, 3) * "2" + + def test_quaternion_div_float(self): + q = Quaternionr(2.0, 4.0, 6.0, 8.0) / 2.0 + self.assertEqual( + (q.x_val, q.y_val, q.z_val, q.w_val), + (1.0, 2.0, 3.0, 4.0), + ) + + def test_quaternion_div_numpy_uint(self): + q = Quaternionr(2.0, 4.0, 6.0, 8.0) / np.uint32(2) + self.assertEqual( + (float(q.x_val), float(q.y_val), float(q.z_val), float(q.w_val)), + (1.0, 2.0, 3.0, 4.0), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/PythonClient/airsim/types.py b/PythonClient/airsim/types.py index 7aef005549..1f36409614 100644 --- a/PythonClient/airsim/types.py +++ b/PythonClient/airsim/types.py @@ -1,8 +1,20 @@ from __future__ import print_function -import msgpackrpc #install as admin: pip install msgpack-rpc-python +try: + import msgpackrpc # install as admin: pip install msgpack-rpc-python +except ImportError: # allow math type unit tests without full RPC stack + msgpackrpc = None import numpy as np #pip install numpy import math +def _is_real_number(value): + """True for Python/NumPy real scalars (NumPy 2 removed np.sctypes).""" + if isinstance(value, (int, float)): + return True + if isinstance(value, (np.integer, np.floating)): + return True + return False + + class MsgpackMixin: def __repr__(self): from pprint import pformat @@ -110,13 +122,13 @@ def __sub__(self, other): return Vector3r(self.x_val - other.x_val, self.y_val - other.y_val, self.z_val - other.z_val) def __truediv__(self, other): - if type(other) in [int, float] + np.sctypes['int'] + np.sctypes['uint'] + np.sctypes['float']: + if _is_real_number(other): return Vector3r( self.x_val / other, self.y_val / other, self.z_val / other) else: raise TypeError('unsupported operand type(s) for /: %s and %s' % ( str(type(self)), str(type(other))) ) def __mul__(self, other): - if type(other) in [int, float] + np.sctypes['int'] + np.sctypes['uint'] + np.sctypes['float']: + if _is_real_number(other): return Vector3r(self.x_val*other, self.y_val*other, self.z_val*other) else: raise TypeError('unsupported operand type(s) for *: %s and %s' % ( str(type(self)), str(type(other))) ) @@ -188,7 +200,7 @@ def __mul__(self, other): def __truediv__(self, other): if type(other) == type(self): return self * other.inverse() - elif type(other) in [int, float] + np.sctypes['int'] + np.sctypes['uint'] + np.sctypes['float']: + elif _is_real_number(other): return Quaternionr( self.x_val / other, self.y_val / other, self.z_val / other, self.w_val / other) else: raise TypeError('unsupported operand type(s) for /: %s and %s' % ( str(type(self)), str(type(other))) )