Skip to content

Commit ee82c51

Browse files
committed
Fix stubbing of callable module descriptors like numpy.vstack
When validating method stubs, callable objects exposing __get__ were treated as non-callable to keep class-descriptor attributes on the property stubbing path. NumPy's _ArrayFunctionDispatcher (used by `np.vstack`) matches that shape, so method stubbing regressed with "is not callable". Pass the active spec into _should_continue_with_stubbed_invocation() and compute descriptor handling inside the helper. Keep the class-spec behavior, but allow callable descriptor-like values for non-class specs (e.g. modules). Add a numpy regression test that stubs np.vstack. Ref #119
1 parent 2a59adb commit ee82c51

2 files changed

Lines changed: 24 additions & 5 deletions

File tree

mockito/mocking.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,11 @@ def _ensure_target_is_callable(theMock: Mock, method_name: str) -> None:
231231
if not was_in_spec and target is None:
232232
return
233233

234-
if _should_continue_with_stubbed_invocation(target, allow_classes=True):
234+
if _should_continue_with_stubbed_invocation(
235+
target,
236+
allow_classes=True,
237+
spec=theMock.spec,
238+
):
235239
return
236240

237241
raise invocation.InvocationError("'%s' is not callable." % method_name)
@@ -253,7 +257,7 @@ def _ensure_target_is_not_callable(theMock: Mock, method_name: str) -> None:
253257
else:
254258
return
255259

256-
if _should_continue_with_stubbed_invocation(value):
260+
if _should_continue_with_stubbed_invocation(value, spec=spec):
257261
raise invocation.InvocationError(
258262
f"expected an invocation of '{method_name}'"
259263
)
@@ -262,6 +266,7 @@ def _ensure_target_is_not_callable(theMock: Mock, method_name: str) -> None:
262266
def _should_continue_with_stubbed_invocation(
263267
value: object,
264268
allow_classes: bool = False,
269+
spec: object | None = None,
265270
) -> bool:
266271
if (
267272
inspect.isfunction(value)
@@ -276,12 +281,21 @@ def _should_continue_with_stubbed_invocation(
276281
):
277282
return True
278283

279-
# Generic callable fallback, but keep custom descriptors/property-like
280-
# attributes on the property stubbing path.
284+
# For class specs, callable descriptors (objects implementing both
285+
# `__call__` and `__get__`) are generally meant to be stubbed through
286+
# the property path. For non-class specs (e.g. module attributes such as
287+
# `numpy.vstack`), `__get__` should not disqualify callable targets.
288+
treat_callable_descriptors_as_non_callable = inspect.isclass(spec)
289+
290+
# Generic callable fallback, with optional handling for callable
291+
# descriptor-like objects (`__call__` + `__get__`).
281292
return (
282293
callable(value)
283294
and (allow_classes or not inspect.isclass(value))
284-
and not hasattr(value, '__get__')
295+
and (
296+
not treat_callable_descriptors_as_non_callable
297+
or not hasattr(value, '__get__')
298+
)
285299
)
286300

287301

tests/numpy_test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ def testEnsureNumpyArrayAllowedWhenCalling(self):
2929
when(module).one_arg(Ellipsis).thenReturn('yep')
3030
assert module.one_arg(array) == 'yep'
3131

32+
33+
def test_np_vstack_is_callable():
34+
when(np).vstack(...).thenReturn("ok.")
35+
36+
assert np.vstack([np.array([1]), np.array([2])]) == "ok."

0 commit comments

Comments
 (0)