decoy icon indicating copy to clipboard operation
decoy copied to clipboard

Add better static typing to Captor

Open Alejandro-FA opened this issue 7 months ago • 1 comments

Summary

Improve static typing support for argument matchers the argument captor.

Note: Feel free to suggest changes or deny the PR. I'm open for discussions.

Motivation and context

One of the selling points of Decoy, at least for me, is its well-typed interface, which improves the developer experience. However, the typing of argument matchers falls a bit short in this regard. The previous implementation relied heavily on using Any as a return type, but it can be improved with the use of Generics.

Description of changes

  1. Change the Captor matcher to provide a typed interface for getting the captured values, instead of having to get them from an object of type Any. By separating the object used to get the captured values and the object passed as a matcher, we get a typed interface with better support for for auto-completions in the IDE. I've also added an optional parameter to specify the type to match.
from decoy import Decoy, matchers

class Dependency():
    def do_thing(self, input: str) -> int:
        return 42

decoy = Decoy()
fake = decoy.mock(cls=Dependency)

captor: ArgumentCaptor[Any] = matchers.argument_captor()
captor_typed: ArgumentCaptor[str] = matchers.argument_captor(str)

decoy.when(fake.do_thing(captor.capture())).then_return(42) # captor.capture() has type Any
decoy.when(fake.do_thing(captor_typed.capture())).then_return(42) # captor.capture() has type str

values: list[Any] = captor.values
values_typed: list[str] = captor_typed.values
  1. On the other hand, I've changed the return type of matchers to use a generic type. For matchers without a clear expected type (like Anything or HasAttributes), I use a single generic type variable as the return type.
  • For pyright (which supports bidirectional inference), this means that the matcher infers the target type based on the context. See an example here. This approach is used by Mockito in Java.

  • For mypy, which is not capable of this type of inference (see an example here), the generic type is assigned its default value, which is Any for backwards compatibility.

This change helps to remove errors and warnings of strict type checkers that report any usage of Any, like basedpyright.

Testing

I have added both unit tests and type tests to verify the new expected behavior. I have also fixed a type test that was failing because of a syntax error.

Documentation

I have updated the documentation with the new functionality. I have also fixed a couple of errors that I found.

Alejandro-FA avatar Jul 06 '25 20:07 Alejandro-FA

Hey, thanks a lot for your feedback and for taking such a good look at this PR. Given your comments, I agree that the changes to matchers introduce many potential breaking changes, so I will reduce the scope of the PR to the argument captor.

Sorry for not responding earlier, I hadn't had the time to review your feedback properly.

Alejandro-FA avatar Aug 21 '25 07:08 Alejandro-FA