Skip to content

Commit

Permalink
Merge pull request #90 from oxan/pep695
Browse files Browse the repository at this point in the history
PEP 695 support
  • Loading branch information
oxan committed Dec 25, 2023
2 parents 77d106f + bc1eb91 commit 0e4c1b8
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 11 deletions.
2 changes: 1 addition & 1 deletion rest_framework_dataclasses/field_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def get_type_info(tp: type) -> TypeInfo:
tp = typing_utils.get_iterable_element_type(tp)

if typing_utils.is_type_variable(tp):
tp = typing_utils.get_variable_type_substitute(tp)
tp = typing_utils.get_type_variable_substitution(tp)

return TypeInfo(is_many, is_mapping, is_final, is_nullable, tp, cp)

Expand Down
18 changes: 11 additions & 7 deletions rest_framework_dataclasses/typing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ def get_resolved_type_hints(tp: type) -> typing.Dict[str, type]:
Resolving the type hints means converting any stringified type hint into an actual type object. These can come from
either forward references (PEP 484), or postponed evaluation (PEP 563).
"""
# typing.get_type_hints() does the heavy lifting for us, except when using PEP 585 generic types that contain a
# stringified type hint (see https://bugs.python.org/issue41370)
# typing.get_type_hints() does the heavy lifting for us, except:
# - when using PEP 585 generic types that contain a stringified type hint, on Python 3.9 and 3.10. See
# https://bugs.python.org/issue41370. Only references to objects in the global namespace are supported here.
# - when using PEP 695 type aliases
def _resolve_type(context_type: type, resolve_type: typing.Union[str, type]) -> type:
if isinstance(resolve_type, str):
globalsns = sys.modules[context_type.__module__].__dict__
Expand All @@ -66,12 +68,14 @@ def _resolve_type(context_type: type, resolve_type: typing.Union[str, type]) ->
return _resolve_type_hint(context_type, resolve_type)

def _resolve_type_hint(context_type: type, resolve_type: type) -> type:
if not hasattr(types, 'GenericAlias') or not isinstance(resolve_type, types.GenericAlias):
if hasattr(types, 'GenericAlias') and isinstance(resolve_type, types.GenericAlias):
args = tuple(_resolve_type(context_type, arg) for arg in resolve_type.__args__)
return typing.cast(type, types.GenericAlias(resolve_type.__origin__, args))
elif hasattr(typing, 'TypeAliasType') and isinstance(resolve_type, typing.TypeAliasType):
return _resolve_type_hint(context_type, resolve_type.__value__)
else:
return resolve_type

args = tuple(_resolve_type(context_type, arg) for arg in resolve_type.__args__)
return typing.cast(type, types.GenericAlias(resolve_type.__origin__, args))

return {k: _resolve_type_hint(tp, v) for k, v in typing.get_type_hints(tp).items()}


Expand Down Expand Up @@ -284,7 +288,7 @@ def is_type_variable(tp: type) -> bool:
return isinstance(tp, typing.TypeVar)


def get_variable_type_substitute(tp: type) -> type:
def get_type_variable_substitution(tp: type) -> type:
"""
Get the substitute for a variable type.
"""
Expand Down
21 changes: 21 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import unittest
import sys


def load_tests(loader: unittest.TestLoader, tests, pattern):
# Manually load tests to avoid loading tests with syntax that's incompatible with the current Python version
for module in (
'test_field_generation',
'test_field_utils',
'test_fields',
'test_functional',
'test_issues',
'test_serializers',
'test_typing_utils',
):
tests.addTests(loader.loadTestsFromName('tests.' + module))

if sys.version_info >= (3, 12, 0):
tests.addTests(loader.loadTestsFromName('tests.test_py312'))

return tests
35 changes: 35 additions & 0 deletions tests/test_py312.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import typing
import unittest
import sys

from rest_framework_dataclasses import typing_utils


@unittest.skipIf(sys.version_info < (3, 12, 0), 'Python 3.12 required')
class Python312Test(unittest.TestCase):
def test_resolve_pep695(self):
type Str = str
type StrList = list[str]
type GenericList[T] = list[T]

class Hinted:
a: Str
b: StrList
c: GenericList

hints = typing_utils.get_resolved_type_hints(Hinted)
self.assertEqual(hints['a'], str)
self.assertEqual(hints['b'], list[str])
self.assertEqual(typing.get_origin(hints['c']), list)

def test_typevar_pep695(self):
type GenericList[T: str] = list[T]
def fn() -> GenericList:
pass

tp = typing_utils.get_resolved_type_hints(fn)['return']

self.assertTrue(typing_utils.is_iterable_type(tp))
element_type = typing_utils.get_iterable_element_type(tp)
self.assertTrue(typing_utils.is_type_variable(element_type))
self.assertEqual(typing_utils.get_type_variable_substitution(element_type), str)
41 changes: 38 additions & 3 deletions tests/test_typing_utils.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,51 @@
import types as types_module
import typing
import unittest
import sys

from rest_framework_dataclasses import types, typing_utils


class GlobalType:
pass


class TypingTest(unittest.TestCase):
def assertAnyTypeEquivalent(self, tp: type):
# In some cases we accept either typing.Any (used by Python 3.9+) or an unconstrained typevar (used by Python
# 3.7 and 3.8). It's essentially the same, and we strip the typevar before usage anyway.
self.assertTrue(tp is typing.Any or (isinstance(tp, typing.TypeVar) and len(tp.__constraints__) == 0))

def test_resolve(self):
class Hinted:
a: str
b: 'str'

hints = typing_utils.get_resolved_type_hints(Hinted)
self.assertEqual(hints['a'], str)
self.assertEqual(hints['b'], str)

@unittest.skipIf(sys.version_info < (3, 9, 0), 'Python 3.9 required')
def test_resolve_pep585(self):
# Pre-Python 3.11 only references to the global namespace are supported
class Hinted:
a: list[GlobalType]
b: list['GlobalType']

hints = typing_utils.get_resolved_type_hints(Hinted)
self.assertEqual(hints['a'], types_module.GenericAlias(list, (GlobalType, )))
self.assertEqual(hints['b'], types_module.GenericAlias(list, (GlobalType, )))

@unittest.skipIf(sys.version_info < (3, 11, 0), 'Python 3.11 required')
def test_resolve_pep585_full(self):
class Hinted:
a: list[str]
b: list['str']

hints = typing_utils.get_resolved_type_hints(Hinted)
self.assertEqual(hints['a'], types_module.GenericAlias(list, (str, )))
self.assertEqual(hints['b'], types_module.GenericAlias(list, (str, )))

def test_iterable(self):
self.assertTrue(typing_utils.is_iterable_type(typing.Iterable[str]))
self.assertTrue(typing_utils.is_iterable_type(typing.Collection[str]))
Expand Down Expand Up @@ -169,6 +204,6 @@ def test_variable_type(self):
self.assertFalse(typing_utils.is_type_variable(int))
self.assertFalse(typing_utils.is_type_variable(typing.List))

self.assertEqual(typing_utils.get_variable_type_substitute(T), typing.Any)
self.assertEqual(typing_utils.get_variable_type_substitute(U), typing.Union[int, str])
self.assertEqual(typing_utils.get_variable_type_substitute(V), Exception)
self.assertEqual(typing_utils.get_type_variable_substitution(T), typing.Any)
self.assertEqual(typing_utils.get_type_variable_substitution(U), typing.Union[int, str])
self.assertEqual(typing_utils.get_type_variable_substitution(V), Exception)

0 comments on commit 0e4c1b8

Please sign in to comment.