Repository: HypothesisWorks/hypothesis

Issue:
`get_type_hints()` should support `functools.partial()`
Our `get_type_hints()` helper doesn't currently support `functools.partial()`, but I think it should - it's easy enough to detect, recurse on the wrapped callable, and then drop any bound-positional args (bound kwargs just assign a default value).

We should also consider upstreaming this fix into `typing.get_type_hints()`, since `inspect.signature()` does this already.


Discussion:

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/internal/compat.py b/hypothesis-python/src/hypothesis/internal/compat.py
--- a/hypothesis-python/src/hypothesis/internal/compat.py
+++ b/hypothesis-python/src/hypothesis/internal/compat.py
@@ -13,6 +13,7 @@
 import platform
 import sys
 import typing
+from functools import partial
 from typing import Any, ForwardRef, Tuple
 
 try:
@@ -119,6 +120,14 @@ def get_type_hints(thing):
     Never errors: instead of raising TypeError for uninspectable objects, or
     NameError for unresolvable forward references, just return an empty dict.
     """
+    if isinstance(thing, partial):
+        from hypothesis.internal.reflection import get_signature
+
+        bound = set(get_signature(thing.func).parameters).difference(
+            get_signature(thing).parameters
+        )
+        return {k: v for k, v in get_type_hints(thing.func).items() if k not in bound}
+
     kwargs = {} if sys.version_info[:2] < (3, 9) else {"include_extras": True}
 
     try:
diff --git a/hypothesis-python/src/hypothesis/internal/reflection.py b/hypothesis-python/src/hypothesis/internal/reflection.py
--- a/hypothesis-python/src/hypothesis/internal/reflection.py
+++ b/hypothesis-python/src/hypothesis/internal/reflection.py
@@ -24,7 +24,7 @@
 from keyword import iskeyword
 from tokenize import COMMENT, detect_encoding, generate_tokens, untokenize
 from types import ModuleType
-from typing import TYPE_CHECKING, Callable
+from typing import TYPE_CHECKING, Any, Callable
 from unittest.mock import _patch as PatchType
 
 from hypothesis.internal.compat import PYPY, is_typed_named_tuple, update_code_location
@@ -127,7 +127,7 @@ def check_signature(sig: inspect.Signature) -> None:
             )
 
 
-def get_signature(target, *, follow_wrapped=True):
+def get_signature(target: Any, *, follow_wrapped: bool = True) -> inspect.Signature:
     # Special case for use of `@unittest.mock.patch` decorator, mimicking the
     # behaviour of getfullargspec instead of reporting unusable arguments.
     patches = getattr(target, "patchings", None)


Test patch:
diff --git a/hypothesis-python/tests/cover/test_compat.py b/hypothesis-python/tests/cover/test_compat.py
--- a/hypothesis-python/tests/cover/test_compat.py
+++ b/hypothesis-python/tests/cover/test_compat.py
@@ -10,6 +10,7 @@
 
 import math
 from dataclasses import dataclass
+from functools import partial
 from inspect import Parameter, Signature, signature
 from typing import ForwardRef, Optional, Union
 
@@ -88,3 +89,20 @@ class Bar:
 def test_resolve_fwd_refs(obj, expected):
     # See: https://github.com/HypothesisWorks/hypothesis/issues/3519
     assert get_type_hints(obj)["x"] == expected
+
+
+def func(a, b: int, *c: str, d: int = None):
+    pass
+
+
+@pytest.mark.parametrize(
+    "pf, names",
+    [
+        (partial(func, 1), "b c d"),
+        (partial(func, 1, 2), "c d"),
+        (partial(func, 1, 2, 3, 4, 5), "c d"),  # varargs don't fill
+        (partial(func, 1, 2, 3, d=4), "c d"),  # kwonly args just get new defaults
+    ],
+)
+def test_get_hints_through_partial(pf, names):
+    assert set(get_type_hints(pf)) == set(names.split())