Add better static typing to Captor
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
- Change the
Captormatcher to provide a typed interface for getting the captured values, instead of having to get them from an object of typeAny. 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
- 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
AnythingorHasAttributes), 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
Anyfor 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.
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.