Skip to content

Get default type of hybrid_property from type annotation #333

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

Closed
wants to merge 4 commits into from
Closed
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
44 changes: 42 additions & 2 deletions graphene_sqlalchemy/converter.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import datetime
import warnings
from functools import singledispatch

from sqlalchemy import types
Expand Down Expand Up @@ -115,8 +117,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,
Expand Down Expand Up @@ -256,3 +259,40 @@ 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 _check_type(type_, args):
return type_ in (args + [x.__name__ for x in args])


def convert_hybrid_type(type_, column):
Copy link
Member

Choose a reason for hiding this comment

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

See my comment in the PR

Copy link
Contributor

@flipbit03 flipbit03 Apr 28, 2022

Choose a reason for hiding this comment

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

I need this feature, so I picked it up to try to finish the work.

Just as a heads up, I tried to use the actual @singledispatch mechanism and it is not a very good fit, since we are using actual types from a type annotation, and not values. So the types inside a type annotation usually descend from the base type, so @singledispatch fails to match anything in a meaningful way 😢

I'm trying to code an approach similar to that where we can decorate functions and switch based on value or on some kind of matcher function, so it will basically become a glorified (but as you said, customizable by the end user in a very flexible way) switch statement. I foresee we will still need some kind of escape hatch using the 'default' (no match) function to analyze deeper types like List[T] and things like that.

Copy link
Member

Choose a reason for hiding this comment

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

Let me know what you come up with! 😃

Copy link
Contributor

@flipbit03 flipbit03 Apr 28, 2022

Choose a reason for hiding this comment

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

I will, thank you 🙏 I'll open a new Pull Request for #333 from my own fork as soon as I have the basics working.

Copy link
Author

Choose a reason for hiding this comment

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

I want to finish this work. I will work on this PR tomorrow, so can it wait?

Copy link
Author

Choose a reason for hiding this comment

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

I am branching by case statement because it returning the corresponding Graphene type in a class of type type obtained by __annotations__.
functools.singledispatch is a way to achieve polymorphism by instance type, but I don't know at this time how to achieve polymorphism by the class itself.

Copy link
Contributor

@flipbit03 flipbit03 Apr 29, 2022

Choose a reason for hiding this comment

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

Indeed, the standard library's singledispatch approach is inadequate for what we want. I provided a solution using a different type of matching but leaving the comfort/developer a @singledispatch-like experience provides.

I finished the work I intended and opened a pull request, mentioning this issue in it, looking forward for the review.

if _check_type(type_, [str, datetime.date, datetime.time]):
return String
elif _check_type(type_, [datetime.datetime]):
from graphene.types.datetime import DateTime
return DateTime
elif _check_type(type_, [Int]):
return Int
elif _check_type(type_, [float]):
return Float
elif _check_type(type_, [bool]):
return Boolean
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)
return List(inner_type)
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
21 changes: 21 additions & 0 deletions graphene_sqlalchemy/tests/models.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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"
)
Expand Down
51 changes: 48 additions & 3 deletions graphene_sqlalchemy/tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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']
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down