1+ import datetime
2+ import typing
3+ import warnings
4+ from decimal import Decimal
15from functools import singledispatch
6+ from typing import Any
27
38from sqlalchemy import types
49from sqlalchemy .dialects import postgresql
510from sqlalchemy .orm import interfaces , strategies
611
7- from graphene import (ID , Boolean , Dynamic , Enum , Field , Float , Int , List ,
8- String )
12+ from graphene import (ID , Boolean , Date , DateTime , Dynamic , Enum , Field , Float ,
13+ Int , List , String , Time )
914from graphene .types .json import JSONString
1015
1116from .batching import get_batch_resolver
1419 default_connection_field_factory )
1520from .registry import get_global_registry
1621from .resolvers import get_attr_resolver , get_custom_resolver
22+ from .utils import (registry_sqlalchemy_model_from_str , safe_isinstance ,
23+ singledispatchbymatchfunction , value_equals )
24+
25+ try :
26+ from typing import ForwardRef
27+ except ImportError :
28+ # python 3.6
29+ from typing import _ForwardRef as ForwardRef
1730
1831try :
1932 from sqlalchemy_utils import ChoiceType , JSONType , ScalarListType , TSVectorType
2538except ImportError :
2639 EnumTypeImpl = object
2740
28-
2941is_selectin_available = getattr (strategies , 'SelectInLoader' , None )
3042
3143
@@ -48,6 +60,7 @@ def convert_sqlalchemy_relationship(relationship_prop, obj_type, connection_fiel
4860 :param dict field_kwargs:
4961 :rtype: Dynamic
5062 """
63+
5164 def dynamic_type ():
5265 """:rtype: Field|None"""
5366 direction = relationship_prop .direction
@@ -115,8 +128,7 @@ def _convert_o2m_or_m2m_relationship(relationship_prop, obj_type, batching, conn
115128
116129def convert_sqlalchemy_hybrid_method (hybrid_prop , resolver , ** field_kwargs ):
117130 if 'type_' not in field_kwargs :
118- # TODO The default type should be dependent on the type of the property propety.
119- field_kwargs ['type_' ] = String
131+ field_kwargs ['type_' ] = convert_hybrid_property_return_type (hybrid_prop )
120132
121133 return Field (
122134 resolver = resolver ,
@@ -240,7 +252,7 @@ def convert_scalar_list_to_list(type, column, registry=None):
240252
241253
242254def init_array_list_recursive (inner_type , n ):
243- return inner_type if n == 0 else List (init_array_list_recursive (inner_type , n - 1 ))
255+ return inner_type if n == 0 else List (init_array_list_recursive (inner_type , n - 1 ))
244256
245257
246258@convert_sqlalchemy_type .register (types .ARRAY )
@@ -260,3 +272,103 @@ def convert_json_to_string(type, column, registry=None):
260272@convert_sqlalchemy_type .register (JSONType )
261273def convert_json_type_to_string (type , column , registry = None ):
262274 return JSONString
275+
276+
277+ @singledispatchbymatchfunction
278+ def convert_sqlalchemy_hybrid_property_type (arg : Any ):
279+ existing_graphql_type = get_global_registry ().get_type_for_model (arg )
280+ if existing_graphql_type :
281+ return existing_graphql_type
282+
283+ # No valid type found, warn and fall back to graphene.String
284+ warnings .warn (
285+ (f"I don't know how to generate a GraphQL type out of a \" { arg } \" type."
286+ "Falling back to \" graphene.String\" " )
287+ )
288+ return String
289+
290+
291+ @convert_sqlalchemy_hybrid_property_type .register (value_equals (str ))
292+ def convert_sqlalchemy_hybrid_property_type_str (arg ):
293+ return String
294+
295+
296+ @convert_sqlalchemy_hybrid_property_type .register (value_equals (int ))
297+ def convert_sqlalchemy_hybrid_property_type_int (arg ):
298+ return Int
299+
300+
301+ @convert_sqlalchemy_hybrid_property_type .register (value_equals (float ))
302+ def convert_sqlalchemy_hybrid_property_type_float (arg ):
303+ return Float
304+
305+
306+ @convert_sqlalchemy_hybrid_property_type .register (value_equals (Decimal ))
307+ def convert_sqlalchemy_hybrid_property_type_decimal (arg ):
308+ # The reason Decimal should be serialized as a String is because this is a
309+ # base10 type used in things like money, and string allows it to not
310+ # lose precision (which would happen if we downcasted to a Float, for example)
311+ return String
312+
313+
314+ @convert_sqlalchemy_hybrid_property_type .register (value_equals (bool ))
315+ def convert_sqlalchemy_hybrid_property_type_bool (arg ):
316+ return Boolean
317+
318+
319+ @convert_sqlalchemy_hybrid_property_type .register (value_equals (datetime .datetime ))
320+ def convert_sqlalchemy_hybrid_property_type_datetime (arg ):
321+ return DateTime
322+
323+
324+ @convert_sqlalchemy_hybrid_property_type .register (value_equals (datetime .date ))
325+ def convert_sqlalchemy_hybrid_property_type_date (arg ):
326+ return Date
327+
328+
329+ @convert_sqlalchemy_hybrid_property_type .register (value_equals (datetime .time ))
330+ def convert_sqlalchemy_hybrid_property_type_time (arg ):
331+ return Time
332+
333+
334+ @convert_sqlalchemy_hybrid_property_type .register (lambda x : getattr (x , '__origin__' , None ) in [list , typing .List ])
335+ def convert_sqlalchemy_hybrid_property_type_list_t (arg ):
336+ # type is either list[T] or List[T], generic argument at __args__[0]
337+ internal_type = arg .__args__ [0 ]
338+
339+ graphql_internal_type = convert_sqlalchemy_hybrid_property_type (internal_type )
340+
341+ return List (graphql_internal_type )
342+
343+
344+ @convert_sqlalchemy_hybrid_property_type .register (safe_isinstance (ForwardRef ))
345+ def convert_sqlalchemy_hybrid_property_forwardref (arg ):
346+ """
347+ Generate a lambda that will resolve the type at runtime
348+ This takes care of self-references
349+ """
350+
351+ def forward_reference_solver ():
352+ model = registry_sqlalchemy_model_from_str (arg .__forward_arg__ )
353+ if not model :
354+ return String
355+ # Always fall back to string if no ForwardRef type found.
356+ return get_global_registry ().get_type_for_model (model )
357+
358+ return forward_reference_solver
359+
360+
361+ @convert_sqlalchemy_hybrid_property_type .register (safe_isinstance (str ))
362+ def convert_sqlalchemy_hybrid_property_bare_str (arg ):
363+ """
364+ Convert Bare String into a ForwardRef
365+ """
366+
367+ return convert_sqlalchemy_hybrid_property_type (ForwardRef (arg ))
368+
369+
370+ def convert_hybrid_property_return_type (hybrid_prop ):
371+ # Grab the original method's return type annotations from inside the hybrid property
372+ return_type_annotation = hybrid_prop .fget .__annotations__ .get ('return' , str )
373+
374+ return convert_sqlalchemy_hybrid_property_type (return_type_annotation )
0 commit comments