I'm writing some tests for a library using pytest. I want to try a number of test cases for each function exposed by the library, so I've found it convenient to group the tests for each method in a class. All of the functions I want to test have the same signature and return similar results, so I'd like to use a helper method defined in a superclass to do some assertions on the results. A simplified version would run like so:
class MyTestCase:
function_under_test: Optional[Callable[[str], Any]] = None
def assert_something(self, input_str: str, expected_result: Any) -> None:
if self.function_under_test is None:
raise AssertionError(
"To use this helper method, you must set the function_under_test"
"class variable within your test class to the function to be called.")
result = self.function_under_test.__func__(input_str)
assert result == expected_result
# various other assertions on result...
class FunctionATest(MyTestCase):
function_under_test = mymodule.myfunction
def test_whatever(self):
self.assert_something("foo bar baz")
In assert_something, It's necessary to call __func__() on the function since assigning a function to a class attribute makes it a bound method of that class -- otherwise self will be passed through as the first argument to the external library function, where it doesn't make any sense.
This code works as intended. However, it yields the MyPy error:
"Callable[[str], Any]" has no attribute "__func__"
Based on my annotation, it's correct that this isn't a safe operation: an arbitrary Callable may not have a __func__ attribute. However, I can't find any type annotation that would indicate that the function_under_test variable refers to a method and thus will always have __func__. Am I overlooking one, or is there another way to tweak my annotations or accesses to get this working with type-checking?
Certainly, there are plenty of other ways I could get around this, some of which might even be cleaner (use an Any type, skip type checking, use a private method to return the function under test rather than making it a class variable, make the helper method a function, etc.). I'm more interested in whether there's an annotation or other mypy trick that would get this code working.
Callable only makes sure that your object has the __call__ method.
You problem is your call self.function_under_test.__func__(input_str) you should just call your function self.function_under_test(input_str)
See below your example without mypy complaints (v0.910)
from typing import Any, Callable, Optional
class MyTestCase:
function_under_test: Optional[Callable] = None
def myfunction_wrap(self, *args, **kwargs):
raise NotImplementedError
def assert_something(self, input_str: str, expected_result: Any) -> None:
if self.function_under_test is None:
raise AssertionError(
"To use this helper method, you must set the function_under_test"
"class variable within your test class to the function to be called.")
result = self.myfunction_wrap(input_str)
assert result == expected_result
# various other assertions on result...
def myfunction(a: str) -> None:
...
class FunctionATest(MyTestCase):
def myfunction_wrap(self, *args, **kwargs):
myfunction(*args, **kwargs)
def test_whatever(self):
self.assert_something("foo bar baz")
Edit1: missed the point of the questio, moved function inside a wrapper function
There is an annotation that would let mypy understand the method binding... but it just trades one problem for another. It'd be a better idea to redesign your code to avoid the method binding.
The annotation would be
import types
import typing
class MyTestCase:
function_under_test: typing.ClassVar[typing.Optional[types.FunctionType]] = None
specifying that function_under_test is a class variable, not an instance variable, and its value is either an ordinary Python function object or None.
So, for example, this type-checks:
import types
import typing
def foo(arg): pass
class Foo:
x: typing.ClassVar[typing.Optional[types.FunctionType]]
def blah(self) -> None:
reveal_type(self.x)
if self.x is not None:
self.x.__func__() # buggy!
class Bar:
x = foo
and prints
main.py:9: note: Revealed type is "Union[types.MethodType, None]"
Success: no issues found in 1 source file
indicating that mypy recognizes that, if not None, self.x will be a method instead of a function.
But the line marked # buggy! type-checks too, despite passing the wrong arguments. That's because types.FunctionType and types.MethodType have no way to specify a signature. mypy will treat any call as valid, and treat the return type as Any.
So if you'd rather have this problem than your original problem, you can go ahead and use this annotation. But your question already indicates you're aware of other, better solutions, so just use one of those.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With