Repository: openmined/pysyft
Issue: Calling unimplemented numpy methods on tensors causes infinite loop instead of throwing `NotImplemented`

## Description
Calling unimplemented methods such as `np.zeros_like` or `np.full_like` on data pointers causes non terminating recursion instead of throwing `NotImplemented`.

## How to Reproduce

create a data pointer

```py
import syft as sy
import numpy as np

domain_client = sy.login(email="info@openmined.org", password="changethis", port=8081)

arr = np.random.randint(0, 10000, (10000))

data = sy.Tensor(arr).annotate_with_dp_metadata(-1, 11000, data_subjects="Bob")
data_ptr = data.send(domain_client, tags=["my_data"])

domain_client.create_user(name="DS", email="test@email.com", password="password", budget=99999)
ds_client = sy.login(email="test@email.com", password="password", port=8081)

d_ptr = ds_client.store[data_ptr.id_at_location]
```

call `np.zeros_like` which is not implemented

```py
np.zeros_like(d_ptr)
```

```py
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
Cell In [8], line 1
----> 1 np.zeros_like(d_ptr)

File <__array_function__ internals>:180, in zeros_like(*args, **kwargs)

File ~/pysyft/packages/syft/src/syft/core/tensor/passthrough.py:699, in PassthroughTensor.__array_function__(self, func, types, args, kwargs)
    697 if implementation:
    698     return implementation(*args, **kwargs)
--> 699 return self.__class__(func(*args, **kwargs))

File <__array_function__ internals>:180, in zeros_like(*args, **kwargs)

File ~/pysyft/packages/syft/src/syft/core/tensor/passthrough.py:699, in PassthroughTensor.__array_function__(self, func, types, args, kwargs)
    697 if implementation:
    698     return implementation(*args, **kwargs)
--> 699 return self.__class__(func(*args, **kwargs))

    [... skipping similar frames: zeros_like at line 180 (988 times), PassthroughTensor.__array_function__ at line 699 (987 times)]

File ~/pysyft/packages/syft/src/syft/core/tensor/passthrough.py:699, in PassthroughTensor.__array_function__(self, func, types, args, kwargs)
    697 if implementation:
    698     return implementation(*args, **kwargs)
--> 699 return self.__class__(func(*args, **kwargs))

File <__array_function__ internals>:180, in zeros_like(*args, **kwargs)

File ~/pysyft/packages/syft/src/syft/core/tensor/passthrough.py:693, in PassthroughTensor.__array_function__(self, func, types, args, kwargs)
    684 def __array_function__(
    685     self,
    686     func: Callable,
   (...)
    691     # Note: this allows subclasses that don't override
    692     # __array_function__ to handle PassthroughTensor objects.
--> 693     if not all(issubclass(t, self.__class__) for t in types):
    694         return NotImplemented
    696     implementation = query_implementation(self.__class__, func)

RecursionError: maximum recursion depth exceeded while calling a Python object
```

## Expected Behavior
A clear and concise description of what you expected to happen.

## Screenshots
If applicable, add screenshots to help explain your problem.

## System Information
 - OS: [e.g. iOS]
 - OS Version: [e.g. 22]
 - Language Version: [e.g. Python 3.7, Node 10.18.1]
 - Package Manager Version: [e.g. Conda 4.6.1, NPM 6.14.1]
 - Browser (if applicable): [e.g. Google Chrome]
 - Browser Version (if applicable): [e.g. 81.0.4044.138]

## Additional Context
Add any other context about the problem here.


Changed files:
packages/syft/tests/syft/core/tensor/passthrough_test.py, packages/syft/src/syft/core/tensor/passthrough.py

diff --git a/packages/syft/src/syft/core/tensor/passthrough.py b/packages/syft/src/syft/core/tensor/passthrough.py
index c2104bd74..6147f8479 100644
--- a/packages/syft/src/syft/core/tensor/passthrough.py
+++ b/packages/syft/src/syft/core/tensor/passthrough.py
@@ -696,13 +696,20 @@ class PassthroughTensor(np.lib.mixins.NDArrayOperatorsMixin):
         implementation = query_implementation(self.__class__, func)
         if implementation:
             return implementation(*args, **kwargs)
-        return self.__class__(func(*args, **kwargs))
 
-    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
-        implementation = query_implementation(self.__class__, ufunc)
+        method_name = func.__name__
+        implementation = getattr(self.__class__, method_name, None)
         if implementation:
-            return implementation(*inputs, **kwargs)
-        return self.__class__(ufunc(*inputs, **kwargs))
+            return (
+                implementation(*args, **kwargs)
+                if callable(implementation)
+                else self.__getattribute__(method_name)
+            )
+
+        return NotImplemented
+
+    # Set __array_ufunc_ = None for now until we can implement this properly
+    __array_ufunc__ = None
 
     def __repr__(self):
         return f"{self.__class__.__name__}(child={self.child})"
diff --git a/packages/syft/tests/syft/core/tensor/passthrough_test.py b/packages/syft/tests/syft/core/tensor/passthrough_test.py
index dbfd97857..0a4a079ee 100644
--- a/packages/syft/tests/syft/core/tensor/passthrough_test.py
+++ b/packages/syft/tests/syft/core/tensor/passthrough_test.py
@@ -8,6 +8,7 @@ import torch
 
 # syft absolute
 from syft.core.tensor.passthrough import PassthroughTensor
+from syft.core.tensor.tensor import Tensor
 from syft.core.tensor.util import implements
 
 
@@ -933,6 +934,7 @@ def test__array_function__() -> None:
     assert result_c == expected_c.child
 
 
+@pytest.mark.xfail
 def test__array_ufunc__() -> None:
     data_a = np.array([1, 2, 3], dtype=np.int32)
     tensor_a = PtTensorSubclass(child=data_a, unit="Hedgehogs")
@@ -964,6 +966,7 @@ def test_repr() -> None:
     assert type(result) == str
 
 
+@pytest.mark.xfail
 def test_square() -> None:
     data = np.array([[0, 1], [-2, 3]], dtype=np.int32)
     expected = np.array([[0, 1], [4, 9]], dtype=np.int32)
@@ -972,3 +975,20 @@ def test_square() -> None:
     result = np.square(tensor_a)
 
     assert result == tensor_b
+
+
+def test_unimplemented_array_func() -> None:
+    # test if unimplemented ops correctly return TypeError: no implementation found
+    tensor = Tensor(np.array([0, 1, 2, 3], dtype=np.int32))
+
+    # change to some op other than full_like if this ends up being implemented
+    with pytest.raises(TypeError, match="no implementation found"):
+        np.full_like(tensor, 7)
+
+
+def test_unsupported_ufunc() -> None:
+    # we don't currently support ufunc
+    tensor = Tensor(np.array([0, 1, 2, 3], dtype=np.int32))
+
+    with pytest.raises(TypeError, match="does not support ufuncs"):
+        np.sin(tensor)