From 0382353e75b3e2b1f5be9ae887ec97a68a840861 Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Fri, 4 Mar 2022 16:02:56 +0900 Subject: [PATCH 1/4] Get default type of hybrid_property from type annotation --- graphene_sqlalchemy/converter.py | 36 ++++++++++++++++- graphene_sqlalchemy/tests/models.py | 21 ++++++++++ graphene_sqlalchemy/tests/test_types.py | 51 +++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 1720e3d8..37424a5c 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,3 +1,4 @@ +import datetime from functools import singledispatch from sqlalchemy import types @@ -115,8 +116,9 @@ def _convert_o2m_or_m2m_relationship(relationship_prop, obj_type, batching, conn def convert_sqlalchemy_hybrid_method(hybrid_prop, resolver, **field_kwargs): if 'type_' not in field_kwargs: - # TODO The default type should be dependent on the type of the property propety. - field_kwargs['type_'] = String + field_kwargs['type_'] = convert_hybrid_type( + hybrid_prop.fget.__annotations__.get('return', str), hybrid_prop + ) return Field( resolver=resolver, @@ -256,3 +258,33 @@ def convert_json_to_string(type, column, registry=None): @convert_sqlalchemy_type.register(JSONType) def convert_json_type_to_string(type, column, registry=None): return JSONString + + +def convert_hybrid_type(type, column): + if type in [str, datetime.date, datetime.time]: + return String + elif type == datetime.datetime: + from graphene.types.datetime import DateTime + return DateTime + elif type == int: + return Int + elif type == float: + return Float + elif type == bool: + return Boolean + elif getattr(type, "__origin__", None) == list: # check for typing.List[T] + args = getattr(type, "__args__", []) + if len(args) != 1: + return String # Unknown fallback + + inner_type = convert_hybrid_type(args[0], column) + return List(inner_type) + elif getattr(type, "__name__", None) == "list": # check for list[T] + args = getattr(type, "__args__", []) + if len(args) != 1: + return String # Unknown fallback + + inner_type = convert_hybrid_type(args[0], column) + return List(inner_type) + + return String diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index 88e992b9..e72a69fd 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import enum +from typing import List from sqlalchemy import (Column, Date, Enum, ForeignKey, Integer, String, Table, func, select) @@ -69,6 +70,26 @@ class Reporter(Base): def hybrid_prop(self): return self.first_name + @hybrid_property + def hybrid_prop_str(self) -> str: + return self.first_name + + @hybrid_property + def hybrid_prop_int(self) -> int: + return 42 + + @hybrid_property + def hybrid_prop_float(self) -> float: + return 42.3 + + @hybrid_property + def hybrid_prop_bool(self) -> bool: + return True + + @hybrid_property + def hybrid_prop_list(self) -> List[int]: + return [1, 2, 3] + column_prop = column_property( select([func.cast(func.count(id), Integer)]), doc="Column property" ) diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index 32f01509..f771fe50 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -2,8 +2,8 @@ import pytest -from graphene import (Dynamic, Field, GlobalID, Int, List, Node, NonNull, - ObjectType, Schema, String) +from graphene import (Boolean, Dynamic, Field, Float, GlobalID, Int, List, + Node, NonNull, ObjectType, Schema, String) from graphene.relay import Connection from ..converter import convert_sqlalchemy_composite @@ -83,6 +83,11 @@ class Meta: "composite_prop", # Hybrid "hybrid_prop", + "hybrid_prop_str", + "hybrid_prop_int", + "hybrid_prop_float", + "hybrid_prop_bool", + "hybrid_prop_list", # Relationship "pets", "articles", @@ -112,6 +117,36 @@ class Meta: # "doc" is ignored by hybrid_property assert hybrid_prop.description is None + # hybrid_property_str + hybrid_prop_str = ReporterType._meta.fields['hybrid_prop_str'] + assert hybrid_prop_str.type == String + # "doc" is ignored by hybrid_property + assert hybrid_prop_str.description is None + + # hybrid_property_int + hybrid_prop_int = ReporterType._meta.fields['hybrid_prop_int'] + assert hybrid_prop_int.type == Int + # "doc" is ignored by hybrid_property + assert hybrid_prop_int.description is None + + # hybrid_property_float + hybrid_prop_float = ReporterType._meta.fields['hybrid_prop_float'] + assert hybrid_prop_float.type == Float + # "doc" is ignored by hybrid_property + assert hybrid_prop_float.description is None + + # hybrid_property_bool + hybrid_prop_bool = ReporterType._meta.fields['hybrid_prop_bool'] + assert hybrid_prop_bool.type == Boolean + # "doc" is ignored by hybrid_property + assert hybrid_prop_bool.description is None + + # hybrid_property_list + hybrid_prop_list = ReporterType._meta.fields['hybrid_prop_list'] + assert hybrid_prop_list.type == List(Int) + # "doc" is ignored by hybrid_property + assert hybrid_prop_list.description is None + # relationship favorite_article_field = ReporterType._meta.fields['favorite_article'] assert isinstance(favorite_article_field, Dynamic) @@ -179,6 +214,11 @@ class Meta: # Then the automatic SQLAlchemy fields "id", "favorite_pet_kind", + "hybrid_prop_str", + "hybrid_prop_int", + "hybrid_prop_float", + "hybrid_prop_bool", + "hybrid_prop_list", ] first_name_field = ReporterType._meta.fields['first_name'] @@ -276,6 +316,11 @@ class Meta: "favorite_pet_kind", "composite_prop", "hybrid_prop", + "hybrid_prop_str", + "hybrid_prop_int", + "hybrid_prop_float", + "hybrid_prop_bool", + "hybrid_prop_list", "pets", "articles", "favorite_article", @@ -384,7 +429,7 @@ class Meta: assert issubclass(CustomReporterType, ObjectType) assert CustomReporterType._meta.model == Reporter - assert len(CustomReporterType._meta.fields) == 11 + assert len(CustomReporterType._meta.fields) == 16 # Test Custom SQLAlchemyObjectType with Custom Options From 058ed3fcdf7c026fccaa82ee3d5b88f2718e34c8 Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Sat, 5 Mar 2022 15:47:57 +0900 Subject: [PATCH 2/4] use type_ instead of type --- graphene_sqlalchemy/converter.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 37424a5c..3f86efad 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -260,27 +260,27 @@ def convert_json_type_to_string(type, column, registry=None): return JSONString -def convert_hybrid_type(type, column): - if type in [str, datetime.date, datetime.time]: +def convert_hybrid_type(type_, column): + if type_ in [str, datetime.date, datetime.time]: return String - elif type == datetime.datetime: + elif type_ == datetime.datetime: from graphene.types.datetime import DateTime return DateTime - elif type == int: + elif type_ == int: return Int - elif type == float: + elif type_ == float: return Float - elif type == bool: + elif type_ == bool: return Boolean - elif getattr(type, "__origin__", None) == list: # check for typing.List[T] - args = getattr(type, "__args__", []) + elif getattr(type_, "__origin__", None) == list: # check for typing.List[T] + args = getattr(type_, "__args__", []) if len(args) != 1: return String # Unknown fallback inner_type = convert_hybrid_type(args[0], column) return List(inner_type) - elif getattr(type, "__name__", None) == "list": # check for list[T] - args = getattr(type, "__args__", []) + elif getattr(type_, "__name__", None) == "list": # check for list[T] + args = getattr(type_, "__args__", []) if len(args) != 1: return String # Unknown fallback From 8189813ea814203a4d94963e4364d404443c964d Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Sat, 5 Mar 2022 15:59:32 +0900 Subject: [PATCH 3/4] In modern python, instead of a class, it contains class name --- graphene_sqlalchemy/converter.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 3f86efad..e77bd6e1 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -260,26 +260,30 @@ def convert_json_type_to_string(type, column, registry=None): return JSONString +def _check_type(type_, args): + return type_ in (args + [x.__name__ for x in args]) + + def convert_hybrid_type(type_, column): - if type_ in [str, datetime.date, datetime.time]: + if _check_type(type_, [str, datetime.date, datetime.time]): return String - elif type_ == datetime.datetime: + elif _check_type(type_, [datetime.datetime]): from graphene.types.datetime import DateTime return DateTime - elif type_ == int: + elif _check_type(type_, [Int]): return Int - elif type_ == float: + elif _check_type(type_, [float]): return Float - elif type_ == bool: + elif _check_type(type_, [bool]): return Boolean - elif getattr(type_, "__origin__", None) == list: # check for typing.List[T] + elif _check_type(getattr(type_, "__origin__", None), [list]): # check for typing.List[T] args = getattr(type_, "__args__", []) if len(args) != 1: return String # Unknown fallback inner_type = convert_hybrid_type(args[0], column) return List(inner_type) - elif getattr(type_, "__name__", None) == "list": # check for list[T] + elif _check_type(getattr(type_, "__name__", None), [list]): # check for list[T] args = getattr(type_, "__args__", []) if len(args) != 1: return String # Unknown fallback From 9d750304ff1385ffa33c3ef2ab7cccd084b9e514 Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Sat, 5 Mar 2022 16:50:57 +0900 Subject: [PATCH 4/4] show warnings to inform user that type convertion has failed --- graphene_sqlalchemy/converter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index e77bd6e1..d6966fea 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,4 +1,5 @@ import datetime +import warnings from functools import singledispatch from sqlalchemy import types @@ -279,6 +280,7 @@ def convert_hybrid_type(type_, column): elif _check_type(getattr(type_, "__origin__", None), [list]): # check for typing.List[T] args = getattr(type_, "__args__", []) if len(args) != 1: + warnings.warn('seems to typing.List[T] but it has more than one argument', RuntimeWarning, stacklevel=2) return String # Unknown fallback inner_type = convert_hybrid_type(args[0], column) @@ -286,9 +288,11 @@ def convert_hybrid_type(type_, column): elif _check_type(getattr(type_, "__name__", None), [list]): # check for list[T] args = getattr(type_, "__args__", []) if len(args) != 1: + warnings.warn('seems to list[T] but it has more than one argument', RuntimeWarning) return String # Unknown fallback inner_type = convert_hybrid_type(args[0], column) return List(inner_type) + warnings.warn('Could not convert type %s to graphene type' % type_, RuntimeWarning) return String