Skip to content

Commit

Permalink
Merge pull request #3521 from HypothesisWorks/fwd-refs
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Dec 4, 2022
2 parents 7871e88 + f046651 commit d56fe3c
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 3 deletions.
6 changes: 6 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
RELEASE_TYPE: minor

This release improves Hypothesis' ability to resolve forward references in
type annotations. It fixes a bug that prevented
:func:`~hypothesis.strategies.builds` from being used with `pydantic models that
possess updated forward references <https://pydantic-docs.helpmanual.io/usage/postponed_annotations/>`__. See :issue:`3519`.
53 changes: 51 additions & 2 deletions hypothesis-python/src/hypothesis/internal/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,37 @@
import platform
import sys
import typing
from typing import Any, ForwardRef, Tuple

try:
from typing import get_args
except ImportError:
# remove at Python 3.7 end-of-life
from collections.abc import Callable as _Callable

def get_args(
tp: Any,
) -> Tuple[Any, ...]: # pragma: no cover
"""
Examples
--------
>>> assert get_args(int) == ()
>>> assert get_args(Dict[str, int]) == (str, int)
>>> assert get_args(Union[int, Union[T, int], str][int]) == (int, str)
>>> assert get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
>>> assert get_args(Callable[[], T][int]) == ([], int)
"""
if hasattr(tp, "__origin__") and hasattr(tp, "__args__"):
args = tp.__args__
if (
getattr(tp, "__origin__", None) is _Callable
and args
and args[0] is not Ellipsis
):
args = (list(args[:-1]), args[-1])
return args
return ()


try:
BaseExceptionGroup = BaseExceptionGroup
Expand Down Expand Up @@ -71,6 +102,10 @@ def is_typed_named_tuple(cls):
)


def _hint_and_args(x):
return (x,) + get_args(x)


def get_type_hints(thing):
"""Like the typing version, but tries harder and never errors.
Expand Down Expand Up @@ -111,10 +146,24 @@ def get_type_hints(thing):
and is_a_type(p.annotation)
and p.annotation is not p.empty
):

p_hint = p.annotation

# Defer to `get_type_hints` if signature annotation is, or
# contains, a forward reference that is otherwise resolved.
if any(
isinstance(sig_hint, ForwardRef)
and not isinstance(hint, ForwardRef)
for sig_hint, hint in zip(
_hint_and_args(p.annotation),
_hint_and_args(hints.get(p.name, Any)),
)
):
p_hint = hints[p.name]
if p.default is None:
hints[p.name] = typing.Optional[p.annotation]
hints[p.name] = typing.Optional[p_hint]
else:
hints[p.name] = p.annotation
hints[p.name] = p_hint
except (AttributeError, TypeError, NameError): # pragma: no cover
pass

Expand Down
45 changes: 44 additions & 1 deletion hypothesis-python/tests/cover/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
# obtain one at https://mozilla.org/MPL/2.0/.

import math
from inspect import Parameter, Signature
from dataclasses import dataclass
from inspect import Parameter, Signature, signature
from typing import ForwardRef, Optional, Union

import pytest

Expand Down Expand Up @@ -45,3 +47,44 @@ class WeirdSig:

def test_no_type_hints():
assert get_type_hints(WeirdSig) == {}


@dataclass
class Foo:
x: "Foo" = None # type: ignore


Foo.__signature__ = signature(Foo).replace( # type: ignore
parameters=[
Parameter(
"x",
Parameter.POSITIONAL_OR_KEYWORD,
annotation=ForwardRef("Foo"),
default=None,
)
]
)


@dataclass
class Bar:
x: Optional[Union[int, "Bar"]]


Bar.__signature__ = signature(Bar).replace( # type: ignore
parameters=[
Parameter(
"x",
Parameter.POSITIONAL_OR_KEYWORD,
annotation=Optional[Union[int, ForwardRef("Bar")]], # type: ignore
)
]
)


@pytest.mark.parametrize(
"obj,expected", [(Foo, Optional[Foo]), (Bar, Optional[Union[int, Bar]])]
)
def test_resolve_fwd_refs(obj, expected):
# See: https://github.com/HypothesisWorks/hypothesis/issues/3519
assert get_type_hints(obj)["x"] == expected

0 comments on commit d56fe3c

Please sign in to comment.