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

Add support for TypeAliasType to from_type and register_type_strategy #4272

Merged
merged 1 commit into from
Feb 23, 2025
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
3 changes: 3 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RELEASE_TYPE: minor

This releases adds support for type aliases created with the :py:keyword:`type` statement (new in python 3.12) to :func:`~hypothesis.strategies.from_type` and :func:`~hypothesis.strategies.register_type_strategy`.
8 changes: 8 additions & 0 deletions hypothesis-python/src/hypothesis/strategies/_internal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,14 @@ def from_type_guarded(thing):
if strategy is not NotImplemented:
return strategy
return _from_type(thing.__supertype__)
if types.is_a_type_alias_type(
thing
): # pragma: no cover # covered by 3.12+ tests
if thing in types._global_type_lookup:
strategy = as_strategy(types._global_type_lookup[thing], thing)
if strategy is not NotImplemented:
return strategy
return _from_type(thing.__value__)
# Unions are not instances of `type` - but we still want to resolve them!
if types.is_a_union(thing):
args = sorted(thing.__args__, key=types.type_sorting_key)
Expand Down
18 changes: 17 additions & 1 deletion hypothesis-python/src/hypothesis/strategies/_internal/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,30 @@ def is_a_new_type(thing):
return isinstance(thing, typing.NewType)


def is_a_type_alias_type(thing): # pragma: no cover # covered by 3.12+ tests
# TypeAliasType is new in python 3.12, through the type statement. If we're
# before python 3.12 then this can't possibly by a TypeAliasType.
#
# https://docs.python.org/3/reference/simple_stmts.html#type
# https://docs.python.org/3/library/typing.html#typing.TypeAliasType
if sys.version_info < (3, 12):
return False
return isinstance(thing, typing.TypeAliasType)


def is_a_union(thing: object) -> bool:
"""Return True if thing is a typing.Union or types.UnionType (in py310)."""
return isinstance(thing, UnionType) or get_origin(thing) is typing.Union


def is_a_type(thing: object) -> bool:
"""Return True if thing is a type or a generic type like thing."""
return isinstance(thing, type) or is_generic_type(thing) or is_a_new_type(thing)
return (
isinstance(thing, type)
or is_generic_type(thing)
or is_a_new_type(thing)
or is_a_type_alias_type(thing)
)


def is_typing_literal(thing: object) -> bool:
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 @@ -38,6 +38,8 @@
collect_ignore_glob = ["django/*"]
if sys.version_info < (3, 10):
collect_ignore_glob.append("cover/*py310*")
if sys.version_info < (3, 12):
collect_ignore_glob.append("cover/*py312*.py")

if sys.version_info >= (3, 11):
collect_ignore_glob.append("cover/test_asyncio.py") # @asyncio.coroutine removed
Expand Down
67 changes: 67 additions & 0 deletions hypothesis-python/tests/cover/test_typealias_py312.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# 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/.

import pytest

from hypothesis import strategies as st

from tests.common.debug import assert_simple_property, find_any
from tests.common.utils import temp_registered


def test_resolves_simple_typealias():
type MyInt = int
type AliasedInt = MyInt
type MaybeInt = int | None

assert_simple_property(st.from_type(MyInt), lambda x: isinstance(x, int))
assert_simple_property(st.from_type(AliasedInt), lambda x: isinstance(x, int))
assert_simple_property(
st.from_type(MaybeInt), lambda x: isinstance(x, int) or x is None
)

find_any(st.from_type(MaybeInt), lambda x: isinstance(x, int))
find_any(st.from_type(MaybeInt), lambda x: x is None)


def test_resolves_nested():
type Point1 = int
type Point2 = Point1
type Point3 = Point2

assert_simple_property(st.from_type(Point3), lambda x: isinstance(x, int))


def test_mutually_recursive_fails():
# example from
# https://docs.python.org/3/library/typing.html#typing.TypeAliasType.__value__
type A = B
type B = A

# I guess giving a nicer error here would be good, but detecting this in general
# is...complicated.
with pytest.raises(RecursionError):
find_any(st.from_type(A))


def test_can_register_typealias():
type A = int
st.register_type_strategy(A, st.just("a"))
assert_simple_property(st.from_type(A), lambda x: x == "a")


def test_prefers_manually_registered_typealias():
# manually registering a `type A = ...` should override automatic detection
type A = int

assert_simple_property(st.from_type(A), lambda x: isinstance(x, int))

with temp_registered(A, st.booleans()):
assert_simple_property(st.from_type(A), lambda x: isinstance(x, bool))