Repository: pyqtgraph/pyqtgraph

Issue:
Segfault when iterating on a transform-mapped Vector in a thread
<!-- In the following, please describe your issue in detail! -->
<!-- If some sections do not apply, just remove them. -->

### Short description
Take a Vector, map it with a transform, put it in a _threading_ Thread, iterate, and BOOM.

### Code to reproduce
<!-- Please provide a minimal working example that reproduces the issue in the code block below.
     Ideally, this should be a full example someone else could run without additional setup. -->

```python
import pyqtgraph as pg
import threading


def do_it():
    xform = pg.SRTTransform3D()
    v = xform.map(pg.Vector((0, 0, 0)))
    tuple(v)


pg.mkQApp()
threading.Thread(target=do_it).start()
```

### Expected behavior
<!-- What should happen? -->

### Real behavior
<!-- What happens? -->
`SIGSEGV`

### Tested environment(s)

 * PyQtGraph version: 0.13.8dev0
 * Qt Python binding: PyQt5 5.15.10 Qt 5.15.2
 * Python version: 3.11
 * NumPy version: 1.26.3
 * Operating system: linux
 * Installation method: git

### Additional context

Workaround: use a `QThread`, and this isn't an issue.


Discussion:
the output of `map` is lazy, so likely the issue is something in the mapping method.

Let's try and narrow it down a bit, try:

```python
def do_it():
    xform = pg.SRTTransform3D()
    v = xform.map(QtGui.QVector3D(0, 0, 0))
    tuple(v)
```
`QtGui.QVector3D` still segfaults. `np.array` does not.
It still segfaults if I change vector to use `__next__` instead of yielding in the `__iter__`. It still segfaults with python 3.11.9 and pyqt 6.7.0.
This already segfaults, no threading needed.
```python
from PyQt6 import QtGui
import pyqtgraph as pg
m = QtGui.QMatrix4x4()
v0 = pg.Vector()
v1 = m.map(v0)
print(type(v0), type(v1)) # <class 'pyqtgraph.Vector.Vector'> <class 'PyQt6.QtGui.QVector3D'>
tuple(v1)
print('BYE')
```
The issue is that `QMatrix4x4` returns the base class `QVector3D` which doesn't support iteration, resulting in a crash in PyQt6.
https://github.com/pyqtgraph/pyqtgraph/blob/7b1510afc7509ed04e08425e630b58b41b6bef8f/pyqtgraph/Transform3D.py#L48

In PySide6, it results in an error:
`TypeError: 'PySide6.QtGui.QVector3D' object is not iterable`

So spooky that the segfault _goes away_ when executed in a QThread; some peculiar memory arrangements to get that behavior.

What do y'all think, should `Transform3D.map` always return `type(obj)`? That's an easy change, but I don't know that my expectations are necessarily valid here. I can make that PR, if that makes sense.
restricting `Transform3D.map(obj)`  to always return `type(obj)` is definitely where I would start, and that matches the `QMatrix4x4.map()` behavior as well.

I haven't had reason to use `Transform3D` at all in my work, but glancing at it, the types of the methods added are all over the place, so I would not only restrict it to return `type(obj)`, but I would likely restrict the accepted types of `obj` to the types accepted by  `QMatrix4x4`, and add `numpy.ndarray` and `Vector` as well.  I would do away w/ accepting list/tuples.
also if you're changing `.map()` please consider adopting the python 3.10 match/case syntax.

EDIT: also add test cases to 😆 

Resolving patch:
diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py
--- a/pyqtgraph/SRTTransform3D.py
+++ b/pyqtgraph/SRTTransform3D.py
@@ -214,17 +214,6 @@ def update(self):
 
     def __repr__(self):
         return str(self.saveState())
-        
-    def matrix(self, nd=3):
-        if nd == 3:
-            return np.array(self.copyDataTo()).reshape(4,4)
-        elif nd == 2:
-            m = np.array(self.copyDataTo()).reshape(4,4)
-            m[2] = m[3]
-            m[:,2] = m[:,3]
-            return m[:3,:3]
-        else:
-            raise Exception("Argument 'nd' must be 2 or 3")
 
     def __reduce__(self):
         return SRTTransform3D, (self.saveState(),)
diff --git a/pyqtgraph/Transform3D.py b/pyqtgraph/Transform3D.py
--- a/pyqtgraph/Transform3D.py
+++ b/pyqtgraph/Transform3D.py
@@ -30,7 +30,7 @@ def matrix(self, nd=3):
             return m[:3,:3]
         else:
             raise Exception("Argument 'nd' must be 2 or 3")
-        
+
     def map(self, obj):
         """
         Extends QMatrix4x4.map() to allow mapping (3, ...) arrays of coordinates
@@ -45,8 +45,11 @@ def map(self, obj):
             v = QtGui.QMatrix4x4.map(self, Vector(obj))
             return type(obj)([v.x(), v.y(), v.z()])[:len(obj)]
         else:
-            return QtGui.QMatrix4x4.map(self, obj)
-            
+            retval = QtGui.QMatrix4x4.map(self, obj)
+            if not isinstance(retval, type(obj)):
+                return type(obj)(retval)
+            return retval
+
     def inverted(self):
         inv, b = QtGui.QMatrix4x4.inverted(self)
         return Transform3D(inv), b


Test patch:
diff --git a/tests/test_srttransform3d.py b/tests/test_srttransform3d.py
--- a/tests/test_srttransform3d.py
+++ b/tests/test_srttransform3d.py
@@ -1,8 +1,9 @@
 import numpy as np
+import pytest
 from numpy.testing import assert_almost_equal, assert_array_almost_equal
 
 import pyqtgraph as pg
-from pyqtgraph.Qt import QtGui
+from pyqtgraph.Qt import QtCore, QtGui
 
 testPoints = np.array([
                        [0, 0, 0],
@@ -27,7 +28,7 @@ def testMatrix():
 
     tr2 = pg.Transform3D(tr)
     assert np.all(tr.matrix() == tr2.matrix())
-    
+
     # This is the most important test:
     # The transition from Transform3D to SRTTransform3D is a tricky one.
     tr3 = pg.SRTTransform3D(tr2)
@@ -36,3 +37,18 @@ def testMatrix():
     assert_array_almost_equal(tr3.getRotation()[1], tr.getRotation()[1])
     assert_array_almost_equal(tr3.getScale(), tr.getScale())
     assert_array_almost_equal(tr3.getTranslation(), tr.getTranslation())
+
+
+@pytest.mark.parametrize("v", [
+    pg.Vector((0, 0, 0)),
+    QtGui.QVector3D(0, 0, 0),
+    np.array((0, 0, 0)),
+    QtCore.QPoint(0, 0),
+    QtCore.QPointF(0.0, 0.0),
+    (0, 0, 0),
+    [0, 0],
+])
+def testMapTypes(v):
+    tr = pg.SRTTransform3D()
+    res = tr.map(v)
+    assert isinstance(res, type(v))