Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved get_type_hints() for classes with forward references #2409

Merged
merged 1 commit into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RELEASE_TYPE: patch

This patch improves the internals of :func:`~hypothesis.strategies.builds` type
inference, to handle recursive forward references in certain dataclasses.
This is useful for e.g. :pypi:`hypothesmith`'s forthcoming :pypi:`LibCST` mode.
25 changes: 18 additions & 7 deletions hypothesis-python/src/hypothesis/internal/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,25 @@ def get_type_hints(thing):
else:

def get_type_hints(thing):
if inspect.isclass(thing) and not hasattr(thing, "__signature__"):
if is_typed_named_tuple(thing):
# Special handling for typing.NamedTuple
return thing._field_types # type: ignore
thing = thing.__init__ # type: ignore
"""Like the typing version, but tries harder and never errors.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now this is a docstring. Can we have these sorts of docs from all of hypothesis.internal please?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂 Be the change you want to see in the world!


Tries harder: if the thing to inspect is a class but typing.get_type_hints
raises an error or returns no hints, then this function will try calling it
on the __init__ method. This second step often helps with user-defined
classes on older versions of Python.

Never errors: instead of raising TypeError for uninspectable objects, or
NameError for unresolvable forward references, just return an empty dict.
"""
try:
return typing.get_type_hints(thing)
except TypeError:
hints = typing.get_type_hints(thing)
except (TypeError, NameError):
hints = {}
if hints or not inspect.isclass(thing):
return hints
try:
return typing.get_type_hints(thing.__init__)
except (TypeError, NameError, AttributeError):
return {}


Expand Down
2 changes: 2 additions & 0 deletions hypothesis-python/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
# Skip collection of tests which require the Django test runner,
# or that don't work on the current version of Python.
collect_ignore_glob = ["django/*"]
if sys.version_info < (3, 6): # Remove after dropping Python 3.5
collect_ignore_glob.append("cover/*py36*")
if sys.version_info < (3, 8):
collect_ignore_glob.append("cover/*py38*")

Expand Down
33 changes: 33 additions & 0 deletions hypothesis-python/tests/cover/test_lookup_py36.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Most of this work is copyright (C) 2013-2020 David R. MacIver
# (david@drmaciver.com), but it contains contributions by others. See
# CONTRIBUTING.rst for a full list of people who may hold copyright, and
# consult the git log if you need to determine who owns an individual
# contribution.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
#
# END HEADER

import typing

import pytest

from hypothesis import given, strategies as st
from hypothesis.internal.compat import PYPY


class TreeForwardRefs(typing.NamedTuple):
val: int
l: typing.Optional["TreeForwardRefs"]
r: typing.Optional["TreeForwardRefs"]


@pytest.mark.skipif(PYPY, reason="pypy36 does not resolve the forward refs")
@given(st.builds(TreeForwardRefs))
def test_resolves_forward_references_outside_annotations(t):
assert isinstance(t, TreeForwardRefs)
12 changes: 12 additions & 0 deletions hypothesis-python/tests/cover/test_lookup_py38.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#
# END HEADER

import dataclasses
import typing

import pytest
Expand Down Expand Up @@ -101,3 +102,14 @@ def test_layered_optional_key_is_optional():
# Optional keys are not currently supported, as PEP-589 leaves no traces
# at runtime. See https://github.com/python/cpython/pull/17214
find_any(from_type(C), lambda d: "b" not in d)


@dataclasses.dataclass()
class Node:
left: typing.Union["Node", int]
right: typing.Union["Node", int]


@given(st.builds(Node))
def test_can_resolve_recursive_dataclass(val):
assert isinstance(val, Node)