Repository: scikit-learn/scikit-learn
Pull request: fixed error that prevented parametrizing custom estimators that implements `__call__`
#### Reference Issues/PRs

Fixes #28859

#### What does this implement/fix? Explain your changes.

Replaced `if callable(obj)` with two separate checks `if isisfunction(obj)` and `if isinstance(obj, partial)`.


Problem statement:
`parametrize_with_checks` fails if custom estimator implements `__call__`
### Describe the bug

Title.

### Steps/Code to Reproduce

```python
from sklearn.utils.estimator_checks import parametrize_with_checks


class MyEstimator:
    """Dummy estimator."""

    def get_params(self, *, deep=True):
        return {}

    def set_params(self, **kwargs):
        pass

    def fit(self, X, y=None):
        return self

    def fit_transform(self, X, y=None):
        return self.fit(X, y).transform(X)

    def transform(self, X):
        return X

    def __call__(self, X):
        return self.transform(X)


@parametrize_with_checks([MyEstimator()])
def test_sklearn_compatibility(estimator, check):
    check(estimator)
```

### Expected Results

The tests should run.

### Actual Results

Parametrizing the tests fails, because `_get_check_estimator_ids` thinks it's a function tries to look up `obj.__name__`, which doesn't exist.

### Versions

```shell
System:
    python: 3.11.6 (main, Oct 16 2023, 19:37:59) [GCC 11.4.0]
executable: /home/rscholz/Projects/KIWI/tsdm/.venv/bin/python
   machine: Linux-6.5.0-27-generic-x86_64-with-glibc2.35

Python dependencies:
      sklearn: 1.4.2
          pip: 24.0
   setuptools: 69.5.1
        numpy: 1.26.4
        scipy: 1.13.0
       Cython: None
       pandas: 2.2.2
   matplotlib: 3.8.4
       joblib: 1.4.0
threadpoolctl: 3.4.0

Built with OpenMP: True

threadpoolctl info:
       user_api: openmp
   internal_api: openmp
    num_threads: 24
         prefix: libgomp
       filepath: /home/rscholz/Projects/KIWI/tsdm/.venv/lib/python3.11/site-packages/scikit_learn.libs/libgomp-a34b3233.so.1.0.0
        version: None

       user_api: blas
   internal_api: openblas
    num_threads: 24
         prefix: libopenblas
       filepath: /home/rscholz/Projects/KIWI/tsdm/.venv/lib/python3.11/site-packages/numpy.libs/libopenblas64_p-r0-0cf96a72.3.23.dev.so
        version: 0.3.23.dev
threading_layer: pthreads
   architecture: Zen

       user_api: blas
   internal_api: openblas
    num_threads: 24
         prefix: libopenblas
       filepath: /home/rscholz/Projects/KIWI/tsdm/.venv/lib/python3.11/site-packages/scipy.libs/libopenblasp-r0-24bff013.3.26.dev.so
        version: 0.3.26.dev
threading_layer: pthreads
   architecture: Zen
```

Hints from issue discussion:

Commit message: fixed error that prevented parametrizing custom estimators that implement __call__

Patch to review:
diff --git a/sklearn/tests/test_common.py b/sklearn/tests/test_common.py
index 4564eddee410c..ab9be06f4b579 100644
--- a/sklearn/tests/test_common.py
+++ b/sklearn/tests/test_common.py
@@ -103,6 +103,31 @@ def _sample_func(x, y=1):
     pass
 
 
+class EstimatorStub:
+    """Dummy development stub for an estimator."""
+
+    def get_params(self, *, deep=True):
+        return {}
+
+    def set_params(self, **kwargs):
+        pass
+
+    def fit(self, X, y=None):
+        return self
+
+    def fit_transform(self, X, y=None):
+        return self.fit(X, y).transform(X)
+
+    def transform(self, X):
+        return X
+
+    def __call__(self, X):
+        return self.transform(X)
+
+    def __str__(self):
+        return type(self).__name__
+
+
 @pytest.mark.parametrize(
     "val, expected",
     [
@@ -122,6 +147,7 @@ def _sample_func(x, y=1):
                 "solver='newton-cg',warm_start=True)"
             ),
         ),
+        (EstimatorStub(), "EstimatorStub"),
     ],
 )
 def test_get_check_estimator_ids(val, expected):
diff --git a/sklearn/utils/estimator_checks.py b/sklearn/utils/estimator_checks.py
index fb60f206bea66..daacac9398f96 100644
--- a/sklearn/utils/estimator_checks.py
+++ b/sklearn/utils/estimator_checks.py
@@ -9,7 +9,7 @@
 from contextlib import nullcontext
 from copy import deepcopy
 from functools import partial, wraps
-from inspect import signature
+from inspect import isfunction, signature
 from numbers import Integral, Real
 
 import joblib
@@ -405,13 +405,11 @@ def _get_check_estimator_ids(obj):
     --------
     check_estimator
     """
-    if callable(obj):
-        if not isinstance(obj, partial):
-            return obj.__name__
-
+    if isfunction(obj):
+        return obj.__name__
+    if isinstance(obj, partial):
         if not obj.keywords:
             return obj.func.__name__
-
         kwstring = ",".join(["{}={}".format(k, v) for k, v in obj.keywords.items()])
         return "{}({})".format(obj.func.__name__, kwstring)
     if hasattr(obj, "get_params"):


Code review comments:
diff_hunk: @@ -103,6 +103,31 @@ def _sample_func(x, y=1):
     pass
 
 
+class EstimatorStub:
path: sklearn/tests/test_common.py
text: @user1:
```suggestion
class CallableEstimator:
```

diff_hunk: @@ -122,6 +147,7 @@ def _sample_func(x, y=1):
                 "solver='newton-cg',warm_start=True)"
             ),
         ),
+        (EstimatorStub(), "EstimatorStub"),
path: sklearn/tests/test_common.py
text: @user1:
```suggestion
        (EstimatorStub(), "CallableEstimator"),
```