From 0cb60ade0035f5b261e04c7c9742dc87ae1228e0 Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Fri, 4 Mar 2022 16:02:56 +0900 Subject: [PATCH 01/19] 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 04061801..baa8b1f8 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, @@ -260,3 +262,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 1f15fa1a..4601b65d 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -4,8 +4,8 @@ import sqlalchemy.exc import sqlalchemy.orm.exc -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 .. import utils @@ -86,6 +86,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", @@ -115,6 +120,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) @@ -182,6 +217,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'] @@ -279,6 +319,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", @@ -387,7 +432,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 c6089e1eb9b6125ac92644fc4790e713bcfea6a0 Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Sat, 5 Mar 2022 15:47:57 +0900 Subject: [PATCH 02/19] 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 baa8b1f8..0b93ebf8 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -264,27 +264,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 1d754e048e483a378274afa5ee9c1227cabb61fa Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Sat, 5 Mar 2022 15:59:32 +0900 Subject: [PATCH 03/19] 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 0b93ebf8..eaf53a05 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -264,26 +264,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 8f6fad8ab96d263bd5b1832fc4df720788cd10bb Mon Sep 17 00:00:00 2001 From: Naoya Yamashita Date: Sat, 5 Mar 2022 16:50:57 +0900 Subject: [PATCH 04/19] 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 eaf53a05..22c74a66 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 @@ -283,6 +284,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) @@ -290,9 +292,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 From 95b7012d7dfb882529b4bf3e12ba31df143bbd63 Mon Sep 17 00:00:00 2001 From: Cadu Date: Thu, 28 Apr 2022 23:17:59 -0300 Subject: [PATCH 05/19] Implemented matcher-based @singledispatch lookalike :-) --- graphene_sqlalchemy/utils.py | 38 +++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/utils.py b/graphene_sqlalchemy/utils.py index 340ad47e..f562ff83 100644 --- a/graphene_sqlalchemy/utils.py +++ b/graphene_sqlalchemy/utils.py @@ -1,5 +1,7 @@ import re import warnings +from collections import OrderedDict +from typing import Any, Callable, Dict import pkg_resources from sqlalchemy.exc import ArgumentError @@ -87,7 +89,6 @@ def _deprecated_default_symbol_name(column_name, sort_asc): def _deprecated_object_type_for_model(cls, name): - try: return _deprecated_object_type_cache[cls, name] except KeyError: @@ -152,3 +153,38 @@ def sort_argument_for_model(cls, has_default=True): def is_sqlalchemy_version_less_than(version_string): """Check the installed SQLAlchemy version""" return pkg_resources.get_distribution('SQLAlchemy').parsed_version < pkg_resources.parse_version(version_string) + + +class singledispatchbymatchfunction: + """ + Inspired by @singledispatch, this is a variant that works using a matcher function + instead of relying on the type of the first argument. + The register method can be used to register a new matcher, which is passed as the first argument: + """ + + def __init__(self, default: Callable): + self.registry: Dict[Callable, Callable] = OrderedDict() + self.default = default + + def __call__(self, *args, **kwargs): + for matcher_function, final_method in self.registry.items(): + # Register order is important. First one that matches, runs. + if matcher_function(args[0]): + return final_method(*args, **kwargs) + + # No match, using default. + return self.default(*args, **kwargs) + + def register(self, matcher_function: Callable[[Any], bool]): + + def grab_function_from_outside(f): + self.registry[matcher_function] = f + return self + + return grab_function_from_outside + + +def value_equals(value): + """A simple function that makes the equality based matcher functions for + SingleDispatchByMatchFunction prettier""" + return lambda x: x == value From 8cf39c17843208cc898fcf35f460b5390411076a Mon Sep 17 00:00:00 2001 From: Cadu Date: Thu, 28 Apr 2022 23:19:06 -0300 Subject: [PATCH 06/19] Implemented type generation out of hybrid_property method's type annotation. --- graphene_sqlalchemy/converter.py | 106 +++++++++++++++++++------------ 1 file changed, 64 insertions(+), 42 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 22c74a66..07f8bf16 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,13 +1,13 @@ import datetime -import warnings from functools import singledispatch +from typing import Any from sqlalchemy import types from sqlalchemy.dialects import postgresql from sqlalchemy.orm import interfaces, strategies -from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List, - String) +from graphene import (ID, Boolean, Date, DateTime, Dynamic, Enum, Field, Float, + Int, List, String, Time) from graphene.types.json import JSONString from .batching import get_batch_resolver @@ -16,6 +16,7 @@ default_connection_field_factory) from .registry import get_global_registry from .resolvers import get_attr_resolver, get_custom_resolver +from .utils import singledispatchbymatchfunction, value_equals try: from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, TSVectorType @@ -27,7 +28,6 @@ except ImportError: EnumTypeImpl = object - is_selectin_available = getattr(strategies, 'SelectInLoader', None) @@ -50,6 +50,7 @@ def convert_sqlalchemy_relationship(relationship_prop, obj_type, connection_fiel :param dict field_kwargs: :rtype: Dynamic """ + def dynamic_type(): """:rtype: Field|None""" direction = relationship_prop.direction @@ -117,9 +118,7 @@ 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: - field_kwargs['type_'] = convert_hybrid_type( - hybrid_prop.fget.__annotations__.get('return', str), hybrid_prop - ) + field_kwargs['type_'] = convert_hybrid_property_return_type(hybrid_prop) return Field( resolver=resolver, @@ -243,7 +242,7 @@ def convert_scalar_list_to_list(type, column, registry=None): def init_array_list_recursive(inner_type, n): - return inner_type if n == 0 else List(init_array_list_recursive(inner_type, n-1)) + return inner_type if n == 0 else List(init_array_list_recursive(inner_type, n - 1)) @convert_sqlalchemy_type.register(types.ARRAY) @@ -265,38 +264,61 @@ 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 _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) +@singledispatchbymatchfunction +def convert_hybrid_property_return_type_inner(arg: Any): + existing_graphql_type = get_global_registry().get_type_for_model(arg) + if existing_graphql_type: + return existing_graphql_type + raise Exception(f"I don't know how to generate a GraphQL type out of a \"{arg}\" type") + + +@convert_hybrid_property_return_type_inner.register(value_equals(str)) +def convert_hybrid_property_return_type_inner_str(arg): return String + + +@convert_hybrid_property_return_type_inner.register(value_equals(int)) +def convert_hybrid_property_return_type_inner_int(arg): + return Int + + +@convert_hybrid_property_return_type_inner.register(value_equals(float)) +def convert_hybrid_property_return_type_inner_float(arg): + return Float + + +@convert_hybrid_property_return_type_inner.register(value_equals(bool)) +def convert_hybrid_property_return_type_inner_bool(arg): + return Boolean + + +@convert_hybrid_property_return_type_inner.register(value_equals(datetime.datetime)) +def convert_hybrid_property_return_type_inner_datetime(arg): + return DateTime + + +@convert_hybrid_property_return_type_inner.register(value_equals(datetime.date)) +def convert_hybrid_property_return_type_inner_date(arg): + return Date + + +@convert_hybrid_property_return_type_inner.register(value_equals(datetime.time)) +def convert_hybrid_property_return_type_inner_time(arg): + return Time + + +@convert_hybrid_property_return_type_inner.register(lambda x: getattr(x, '__origin__', None) == list) +def convert_hybrid_property_return_type_inner_list(arg): + # type is either list[T] or List[T], generic argument at __args__[0] + internal_type = arg.__args__[0] + + graphql_internal_type = convert_hybrid_property_return_type_inner(internal_type) + + return List(graphql_internal_type) + + +def convert_hybrid_property_return_type(hybrid_prop): + # Grab the original method's return type annotations from inside the hybrid property + return_type_annotation = hybrid_prop.fget.__annotations__.get('return', str) + + return convert_hybrid_property_return_type_inner(return_type_annotation) From 3216608c23e7ae7e842b60b964bceb21121374c4 Mon Sep 17 00:00:00 2001 From: Cadu Date: Thu, 28 Apr 2022 23:19:31 -0300 Subject: [PATCH 07/19] Fixed tests (phew!!!) --- graphene_sqlalchemy/tests/models.py | 41 ++- graphene_sqlalchemy/tests/test_batching.py | 247 +++++++++--------- graphene_sqlalchemy/tests/test_benchmark.py | 28 +- graphene_sqlalchemy/tests/test_converter.py | 11 + graphene_sqlalchemy/tests/test_query.py | 48 ++-- graphene_sqlalchemy/tests/test_query_enums.py | 8 +- graphene_sqlalchemy/tests/test_types.py | 217 +++++++++------ 7 files changed, 353 insertions(+), 247 deletions(-) diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index e72a69fd..bd8e5f94 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import datetime import enum from typing import List @@ -54,6 +55,14 @@ def __repr__(self): return "{} {}".format(self.first_name, self.last_name) +class Article(Base): + __tablename__ = "articles" + id = Column(Integer(), primary_key=True) + headline = Column(String(100)) + pub_date = Column(Date()) + reporter_id = Column(Integer(), ForeignKey("reporters.id")) + + class Reporter(Base): __tablename__ = "reporters" @@ -67,7 +76,7 @@ class Reporter(Base): favorite_article = relationship("Article", uselist=False) @hybrid_property - def hybrid_prop(self): + def hybrid_prop_untyped(self): return self.first_name @hybrid_property @@ -87,9 +96,29 @@ def hybrid_prop_bool(self) -> bool: return True @hybrid_property - def hybrid_prop_list(self) -> List[int]: + def hybrid_prop_list_int(self) -> List[int]: return [1, 2, 3] + @hybrid_property + def hybrid_prop_list_date(self) -> List[datetime.date]: + return [self.hybrid_prop_date, self.hybrid_prop_date, self.hybrid_prop_date] + + @hybrid_property + def hybrid_prop_date(self) -> datetime.date: + return datetime.datetime.now().date() + + @hybrid_property + def hybrid_prop_time(self) -> datetime.time: + return datetime.datetime.now().time() + + @hybrid_property + def hybrid_prop_datetime(self) -> datetime.datetime: + return datetime.datetime.now() + + @hybrid_property + def hybrid_prop_first_article(self) -> Article: + return self.articles[0] + column_prop = column_property( select([func.cast(func.count(id), Integer)]), doc="Column property" ) @@ -97,14 +126,6 @@ def hybrid_prop_list(self) -> List[int]: composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite") -class Article(Base): - __tablename__ = "articles" - id = Column(Integer(), primary_key=True) - headline = Column(String(100)) - pub_date = Column(Date()) - reporter_id = Column(Integer(), ForeignKey("reporters.id")) - - class ReflectedEditor(type): """Same as Editor, but using reflected table.""" diff --git a/graphene_sqlalchemy/tests/test_batching.py b/graphene_sqlalchemy/tests/test_batching.py index 1896900b..12879afe 100644 --- a/graphene_sqlalchemy/tests/test_batching.py +++ b/graphene_sqlalchemy/tests/test_batching.py @@ -17,6 +17,7 @@ class MockLoggingHandler(logging.Handler): """Intercept and store log messages in a list.""" + def __init__(self, *args, **kwargs): self.messages = [] logging.Handler.__init__(self, *args, **kwargs) @@ -42,15 +43,15 @@ def mock_sqlalchemy_logging_handler(): def get_schema(): - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (relay.Node,) batching = True - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (relay.Node,) batching = True @@ -82,11 +83,11 @@ async def test_many_to_one(session_factory): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -138,20 +139,20 @@ async def test_many_to_one(session_factory): assert not result.errors result = to_std_dicts(result.data) assert result == { - "articles": [ - { - "headline": "Article_1", - "reporter": { - "firstName": "Reporter_1", - }, - }, - { - "headline": "Article_2", - "reporter": { - "firstName": "Reporter_2", - }, - }, - ], + "articles": [ + { + "headline": "Article_1", + "reporter": { + "firstName": "Reporter_1", + }, + }, + { + "headline": "Article_2", + "reporter": { + "firstName": "Reporter_2", + }, + }, + ], } @@ -160,11 +161,11 @@ async def test_one_to_one(session_factory): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -216,20 +217,20 @@ async def test_one_to_one(session_factory): assert not result.errors result = to_std_dicts(result.data) assert result == { - "reporters": [ - { - "firstName": "Reporter_1", - "favoriteArticle": { - "headline": "Article_1", - }, - }, - { - "firstName": "Reporter_2", - "favoriteArticle": { - "headline": "Article_2", - }, - }, - ], + "reporters": [ + { + "firstName": "Reporter_1", + "favoriteArticle": { + "headline": "Article_1", + }, + }, + { + "firstName": "Reporter_2", + "favoriteArticle": { + "headline": "Article_2", + }, + }, + ], } @@ -238,11 +239,11 @@ async def test_one_to_many(session_factory): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -306,42 +307,42 @@ async def test_one_to_many(session_factory): assert not result.errors result = to_std_dicts(result.data) assert result == { - "reporters": [ - { - "firstName": "Reporter_1", - "articles": { - "edges": [ - { - "node": { - "headline": "Article_1", - }, - }, - { - "node": { - "headline": "Article_2", - }, - }, - ], - }, - }, - { - "firstName": "Reporter_2", - "articles": { - "edges": [ - { - "node": { - "headline": "Article_3", + "reporters": [ + { + "firstName": "Reporter_1", + "articles": { + "edges": [ + { + "node": { + "headline": "Article_1", + }, + }, + { + "node": { + "headline": "Article_2", + }, + }, + ], }, - }, - { - "node": { - "headline": "Article_4", + }, + { + "firstName": "Reporter_2", + "articles": { + "edges": [ + { + "node": { + "headline": "Article_3", + }, + }, + { + "node": { + "headline": "Article_4", + }, + }, + ], }, - }, - ], - }, - }, - ], + }, + ], } @@ -350,11 +351,11 @@ async def test_many_to_many(session_factory): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -420,42 +421,42 @@ async def test_many_to_many(session_factory): assert not result.errors result = to_std_dicts(result.data) assert result == { - "reporters": [ - { - "firstName": "Reporter_1", - "pets": { - "edges": [ - { - "node": { - "name": "Pet_1", + "reporters": [ + { + "firstName": "Reporter_1", + "pets": { + "edges": [ + { + "node": { + "name": "Pet_1", + }, + }, + { + "node": { + "name": "Pet_2", + }, + }, + ], }, - }, - { - "node": { - "name": "Pet_2", + }, + { + "firstName": "Reporter_2", + "pets": { + "edges": [ + { + "node": { + "name": "Pet_3", + }, + }, + { + "node": { + "name": "Pet_4", + }, + }, + ], }, - }, - ], - }, - }, - { - "firstName": "Reporter_2", - "pets": { - "edges": [ - { - "node": { - "name": "Pet_3", - }, - }, - { - "node": { - "name": "Pet_4", - }, - }, - ], - }, - }, - ], + }, + ], } @@ -468,6 +469,11 @@ def test_disable_batching_via_ormfield(session_factory): session.commit() session.close() + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (relay.Node,) + class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -477,11 +483,6 @@ class Meta: favorite_article = ORMField(batching=False) articles = ORMField(batching=False) - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (relay.Node,) - class Query(graphene.ObjectType): reporters = graphene.Field(graphene.List(ReporterType)) @@ -541,6 +542,11 @@ async def test_connection_factory_field_overrides_batching_is_false(session_fact session.commit() session.close() + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (relay.Node,) + class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -550,11 +556,6 @@ class Meta: articles = ORMField(batching=False) - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (relay.Node,) - class Query(graphene.ObjectType): reporters = graphene.Field(graphene.List(ReporterType)) @@ -600,6 +601,11 @@ def test_connection_factory_field_overrides_batching_is_true(session_factory): session.commit() session.close() + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (relay.Node,) + class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -609,11 +615,6 @@ class Meta: articles = ORMField(batching=True) - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (relay.Node,) - class Query(graphene.ObjectType): reporters = graphene.Field(graphene.List(ReporterType)) diff --git a/graphene_sqlalchemy/tests/test_benchmark.py b/graphene_sqlalchemy/tests/test_benchmark.py index 11e9d0e0..834b7f04 100644 --- a/graphene_sqlalchemy/tests/test_benchmark.py +++ b/graphene_sqlalchemy/tests/test_benchmark.py @@ -12,14 +12,14 @@ def get_schema(): - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (relay.Node,) - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (relay.Node,) class PetType(SQLAlchemyObjectType): @@ -46,8 +46,8 @@ def benchmark_query(session_factory, benchmark, query): @benchmark def execute_query(): result = schema.execute( - query, - context_value={"session": session_factory()}, + query, + context_value={"session": session_factory()}, ) assert not result.errors @@ -56,11 +56,11 @@ def test_one_to_one(session_factory, benchmark): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -91,11 +91,11 @@ def test_many_to_one(session_factory, benchmark): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -126,11 +126,11 @@ def test_one_to_many(session_factory, benchmark): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -173,11 +173,11 @@ def test_many_to_many(session_factory, benchmark): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 57c43058..0809312e 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -9,6 +9,7 @@ from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType import graphene +from graphene import relay from graphene.relay import Node from graphene.types.datetime import DateTime from graphene.types.json import JSONString @@ -243,6 +244,11 @@ class Meta: def test_should_manytoone_convert_connectionorlist_list(): + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (relay.Node,) + class A(SQLAlchemyObjectType): class Meta: model = Reporter @@ -257,6 +263,11 @@ class Meta: def test_should_manytoone_convert_connectionorlist_connection(): + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (relay.Node,) + class A(SQLAlchemyObjectType): class Meta: model = Reporter diff --git a/graphene_sqlalchemy/tests/test_query.py b/graphene_sqlalchemy/tests/test_query.py index 39140814..f6b673cf 100644 --- a/graphene_sqlalchemy/tests/test_query.py +++ b/graphene_sqlalchemy/tests/test_query.py @@ -1,4 +1,5 @@ import graphene +from graphene import relay from graphene.relay import Node from ..converter import convert_sqlalchemy_composite @@ -36,6 +37,11 @@ def test_query_fields(session): def convert_composite_class(composite, registry): return graphene.String() + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (relay.Node,) + class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -55,7 +61,7 @@ def resolve_reporters(self, _info): reporter { firstName columnProp - hybridProp + hybridPropUntyped compositeProp } reporters { @@ -66,7 +72,7 @@ def resolve_reporters(self, _info): expected = { "reporter": { "firstName": "John", - "hybridProp": "John", + "hybridPropUntyped": "John", "columnProp": 2, "compositeProp": "John Doe", }, @@ -82,6 +88,11 @@ def resolve_reporters(self, _info): def test_query_node(session): add_test_data(session) + class ArticleNode(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class ReporterNode(SQLAlchemyObjectType): class Meta: model = Reporter @@ -91,11 +102,6 @@ class Meta: def get_node(cls, info, id): return Reporter(id=2, first_name="Cookie Monster") - class ArticleNode(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class Query(graphene.ObjectType): node = Node.Field() reporter = graphene.Field(ReporterNode) @@ -158,23 +164,23 @@ def test_orm_field(session): def convert_composite_class(composite, registry): return graphene.String() + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter interfaces = (Node,) first_name_v2 = ORMField(model_attr='first_name') - hybrid_prop_v2 = ORMField(model_attr='hybrid_prop') + hybrid_prop_untyped_v2 = ORMField(model_attr='hybrid_prop_untyped') column_prop_v2 = ORMField(model_attr='column_prop') composite_prop = ORMField() favorite_article_v2 = ORMField(model_attr='favorite_article') articles_v2 = ORMField(model_attr='articles') - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) @@ -185,13 +191,13 @@ def resolve_reporter(self, _info): query { reporter { firstNameV2 - hybridPropV2 + hybridPropUntypedV2 columnPropV2 compositeProp favoriteArticleV2 { headline } - articlesV2(first: 1) { + articlesV2 { edges { node { headline @@ -204,7 +210,7 @@ def resolve_reporter(self, _info): expected = { "reporter": { "firstNameV2": "John", - "hybridPropV2": "John", + "hybridPropUntypedV2": "John", "columnPropV2": 2, "compositeProp": "John Doe", "favoriteArticleV2": {"headline": "Hi!"}, @@ -262,6 +268,11 @@ class Query(graphene.ObjectType): def test_mutation(session): add_test_data(session) + class ArticleNode(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class EditorNode(SQLAlchemyObjectType): class Meta: model = Editor @@ -276,11 +287,6 @@ class Meta: def get_node(cls, id, info): return Reporter(id=2, first_name="Cookie Monster") - class ArticleNode(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class CreateArticle(graphene.Mutation): class Arguments: headline = graphene.String() diff --git a/graphene_sqlalchemy/tests/test_query_enums.py b/graphene_sqlalchemy/tests/test_query_enums.py index 5166c45f..07c540cb 100644 --- a/graphene_sqlalchemy/tests/test_query_enums.py +++ b/graphene_sqlalchemy/tests/test_query_enums.py @@ -1,7 +1,8 @@ import graphene +from graphene import Node from ..types import SQLAlchemyObjectType -from .models import HairKind, Pet, Reporter +from .models import Article, HairKind, Pet, Reporter from .test_query import add_test_data, to_std_dicts @@ -13,6 +14,11 @@ class PetType(SQLAlchemyObjectType): class Meta: model = Pet + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index 4601b65d..90e05b96 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -4,8 +4,9 @@ import sqlalchemy.exc import sqlalchemy.orm.exc -from graphene import (Boolean, Dynamic, Field, Float, GlobalID, Int, List, - Node, NonNull, ObjectType, Schema, String) +from graphene import (Boolean, Date, DateTime, Dynamic, Field, Float, GlobalID, + Int, List, Node, NonNull, ObjectType, Schema, String, + Time) from graphene.relay import Connection from .. import utils @@ -34,6 +35,11 @@ class Meta: def test_sqlalchemy_node(session): + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -51,6 +57,11 @@ class Meta: def test_connection(): + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -64,17 +75,17 @@ def test_sqlalchemy_default_fields(): def convert_composite_class(composite, registry): return String() - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (Node,) - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (Node,) - assert list(ReporterType._meta.fields.keys()) == [ + assert sorted(list(ReporterType._meta.fields.keys())) == sorted([ # Columns "column_prop", # SQLAlchemy retuns column properties first "id", @@ -85,70 +96,83 @@ class Meta: # Composite "composite_prop", # Hybrid - "hybrid_prop", + "hybrid_prop_untyped", "hybrid_prop_str", "hybrid_prop_int", "hybrid_prop_float", "hybrid_prop_bool", - "hybrid_prop_list", + "hybrid_prop_list_int", + "hybrid_prop_list_date", + "hybrid_prop_date", + "hybrid_prop_time", + "hybrid_prop_datetime", + "hybrid_prop_first_article", # Relationship "pets", "articles", "favorite_article", - ] + ]) + + # Check field types and descriptions - # column first_name_field = ReporterType._meta.fields['first_name'] assert first_name_field.type == String assert first_name_field.description == "First name" - # column_property + # Column Property column_prop_field = ReporterType._meta.fields['column_prop'] assert column_prop_field.type == Int - # "doc" is ignored by column_property - assert column_prop_field.description is None + assert column_prop_field.description is None # "doc" is ignored by column_property - # composite + # Composite Property full_name_field = ReporterType._meta.fields['composite_prop'] assert full_name_field.type == String - # "doc" is ignored by composite - assert full_name_field.description is None + assert full_name_field.description is None # "doc" is ignored by composite + + # Hybrid Property: Untyped Return (fallback to String) + hybrid_prop_untyped = ReporterType._meta.fields['hybrid_prop_untyped'] + assert hybrid_prop_untyped.type == String + assert hybrid_prop_untyped.description is None # "doc" is ignored by hybrid_property - # hybrid_property - hybrid_prop = ReporterType._meta.fields['hybrid_prop'] - assert hybrid_prop.type == String - # "doc" is ignored by hybrid_property - assert hybrid_prop.description is None + ################################################# + # Hybrid Properties with return type annotations + ################################################# - # 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 + assert hybrid_prop_str.description is None # "doc" is ignored by hybrid_property - # 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 + assert hybrid_prop_int.description is None # "doc" is ignored by hybrid_property - # 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 + assert hybrid_prop_float.description is None # "doc" is ignored by hybrid_property - # 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 + assert hybrid_prop_bool.description is None # "doc" is ignored by hybrid_property - # 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 + hybrid_prop_list_int = ReporterType._meta.fields['hybrid_prop_list_int'] + assert hybrid_prop_list_int.type == List(Int) + assert hybrid_prop_list_int.description is None # "doc" is ignored by hybrid_property + + hybrid_prop_list_date = ReporterType._meta.fields['hybrid_prop_list_date'] + assert hybrid_prop_list_date.type == List(Date) + assert hybrid_prop_list_date.description is None # "doc" is ignored by hybrid_property + + hybrid_prop_date = ReporterType._meta.fields['hybrid_prop_date'] + assert hybrid_prop_date.type == Date + assert hybrid_prop_date.description is None # "doc" is ignored by hybrid_property + + hybrid_prop_time = ReporterType._meta.fields['hybrid_prop_time'] + assert hybrid_prop_time.type == Time + assert hybrid_prop_time.description is None # "doc" is ignored by hybrid_property + + hybrid_prop_datetime = ReporterType._meta.fields['hybrid_prop_datetime'] + assert hybrid_prop_datetime.type == DateTime + assert hybrid_prop_datetime.description is None # "doc" is ignored by hybrid_property # relationship favorite_article_field = ReporterType._meta.fields['favorite_article'] @@ -167,6 +191,11 @@ class ReporterMixin(object): first_name = ORMField(required=True) last_name = ORMField(description='Overridden') + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class ReporterType(SQLAlchemyObjectType, ReporterMixin): class Meta: model = Reporter @@ -183,25 +212,20 @@ class Meta: composite_prop = ORMField() # hybrid_property - hybrid_prop = ORMField(description='Overridden') + hybrid_prop_untyped = ORMField(description='Overridden') # relationships favorite_article = ORMField(description='Overridden') articles = ORMField(deprecation_reason='Overridden') pets = ORMField(description='Overridden') - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class PetType(SQLAlchemyObjectType): class Meta: model = Pet interfaces = (Node,) use_connection = False - assert list(ReporterType._meta.fields.keys()) == [ + assert sorted(list(ReporterType._meta.fields.keys())) == sorted([ # Fields from ReporterMixin "first_name", "last_name", @@ -210,19 +234,25 @@ class Meta: "email_v2", "column_prop", "composite_prop", - "hybrid_prop", "favorite_article", "articles", "pets", # Then the automatic SQLAlchemy fields "id", "favorite_pet_kind", + # Hybrid + "hybrid_prop_untyped", "hybrid_prop_str", "hybrid_prop_int", "hybrid_prop_float", "hybrid_prop_bool", - "hybrid_prop_list", - ] + "hybrid_prop_list_int", + "hybrid_prop_list_date", + "hybrid_prop_date", + "hybrid_prop_time", + "hybrid_prop_datetime", + "hybrid_prop_first_article", + ]) first_name_field = ReporterType._meta.fields['first_name'] assert isinstance(first_name_field.type, NonNull) @@ -245,10 +275,10 @@ class Meta: assert email_field_v2.description == "Email" assert email_field_v2.deprecation_reason is None - hybrid_prop_field = ReporterType._meta.fields['hybrid_prop'] - assert hybrid_prop_field.type == String - assert hybrid_prop_field.description == "Overridden" - assert hybrid_prop_field.deprecation_reason is None + hybrid_prop_untyped_field = ReporterType._meta.fields['hybrid_prop_untyped'] + assert hybrid_prop_untyped_field.type == String + assert hybrid_prop_untyped_field.description == "Overridden" + assert hybrid_prop_untyped_field.deprecation_reason is None column_prop_field_v2 = ReporterType._meta.fields['column_prop'] assert column_prop_field_v2.type == String @@ -303,6 +333,11 @@ class Meta: def test_exclude_fields(): + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -311,23 +346,29 @@ class Meta: first_name = ORMField() # Takes precedence last_name = ORMField() # Noop - assert list(ReporterType._meta.fields.keys()) == [ + assert sorted(list(ReporterType._meta.fields.keys())) == sorted([ "first_name", "last_name", "column_prop", "email", "favorite_pet_kind", "composite_prop", - "hybrid_prop", + "pets", + "articles", + "favorite_article", + # Hybrid + "hybrid_prop_untyped", "hybrid_prop_str", "hybrid_prop_int", "hybrid_prop_float", "hybrid_prop_bool", - "hybrid_prop_list", - "pets", - "articles", - "favorite_article", - ] + "hybrid_prop_list_int", + "hybrid_prop_list_date", + "hybrid_prop_date", + "hybrid_prop_time", + "hybrid_prop_datetime", + "hybrid_prop_first_article", + ]) def test_only_and_exclude_fields(): @@ -341,6 +382,11 @@ class Meta: def test_sqlalchemy_redefine_field(): + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -355,6 +401,11 @@ class Meta: def test_resolvers(session): """Test that the correct resolver functions are called""" + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class ReporterMixin(object): def resolve_id(root, _info): return 'ID' @@ -422,6 +473,11 @@ def resolve_reporter(self, _info): # Test Custom SQLAlchemyObjectType Implementation def test_custom_objecttype_registered(): + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class CustomSQLAlchemyObjectType(SQLAlchemyObjectType): class Meta: abstract = True @@ -432,7 +488,7 @@ class Meta: assert issubclass(CustomReporterType, ObjectType) assert CustomReporterType._meta.model == Reporter - assert len(CustomReporterType._meta.fields) == 16 + assert len(CustomReporterType._meta.fields) == 21 # Test Custom SQLAlchemyObjectType with Custom Options @@ -452,6 +508,11 @@ def __init_subclass_with_meta__(cls, custom_option=None, **options): _meta=_meta, **options ) + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class ReporterWithCustomOptions(SQLAlchemyObjectTypeWithCustomOptions): class Meta: model = Reporter @@ -469,14 +530,14 @@ class _TestSQLAlchemyConnectionField(SQLAlchemyConnectionField): def test_default_connection_field_factory(): - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (Node,) - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (Node,) assert isinstance(ReporterType._meta.fields['articles'].type(), UnsortedSQLAlchemyConnectionField) @@ -488,16 +549,16 @@ def test_connection_field_factory(relationship, registry): _type = registry.get_type_for_model(model) return _TestSQLAlchemyConnectionField(_type._meta.connection) - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (Node,) - connection_field_factory = test_connection_field_factory - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (Node,) + connection_field_factory = test_connection_field_factory assert isinstance(ReporterType._meta.fields['articles'].type(), _TestSQLAlchemyConnectionField) @@ -506,14 +567,14 @@ def test_deprecated_registerConnectionFieldFactory(): with pytest.warns(DeprecationWarning): registerConnectionFieldFactory(_TestSQLAlchemyConnectionField) - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (Node,) - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (Node,) assert isinstance(ReporterType._meta.fields['articles'].type(), _TestSQLAlchemyConnectionField) @@ -524,14 +585,14 @@ def test_deprecated_unregisterConnectionFieldFactory(): registerConnectionFieldFactory(_TestSQLAlchemyConnectionField) unregisterConnectionFieldFactory() - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (Node,) - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (Node,) assert not isinstance(ReporterType._meta.fields['articles'].type(), _TestSQLAlchemyConnectionField) From a7e8a1b9110022de39a98c4693ae199f23130145 Mon Sep 17 00:00:00 2001 From: Cadu Date: Thu, 28 Apr 2022 23:33:02 -0300 Subject: [PATCH 08/19] Made tests also work in py36 (pheww!!! #2) --- graphene_sqlalchemy/converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 07f8bf16..9ffe18b2 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,4 +1,5 @@ import datetime +import typing from functools import singledispatch from typing import Any @@ -307,7 +308,7 @@ def convert_hybrid_property_return_type_inner_time(arg): return Time -@convert_hybrid_property_return_type_inner.register(lambda x: getattr(x, '__origin__', None) == list) +@convert_hybrid_property_return_type_inner.register(lambda x: getattr(x, '__origin__', None) in [list, typing.List]) def convert_hybrid_property_return_type_inner_list(arg): # type is either list[T] or List[T], generic argument at __args__[0] internal_type = arg.__args__[0] From d83f7e1ab97a5edeccea4f48db037f7ca8e7e40b Mon Sep 17 00:00:00 2001 From: Cadu Date: Thu, 28 Apr 2022 23:51:36 -0300 Subject: [PATCH 09/19] Support Decimal() type. --- graphene_sqlalchemy/converter.py | 9 +++++++++ graphene_sqlalchemy/tests/models.py | 5 +++++ graphene_sqlalchemy/tests/test_types.py | 10 +++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 9ffe18b2..b9baed24 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,5 +1,6 @@ import datetime import typing +from decimal import Decimal from functools import singledispatch from typing import Any @@ -288,6 +289,14 @@ def convert_hybrid_property_return_type_inner_float(arg): return Float +@convert_hybrid_property_return_type_inner.register(value_equals(Decimal)) +def convert_hybrid_property_return_type_inner_decimal(arg): + # The reason Decimal should be serialized as a String is because this is a + # base10 type used in things like money, and string allows it to not + # lose precision (which would happen if we downcasted to a Float, for example) + return String + + @convert_hybrid_property_return_type_inner.register(value_equals(bool)) def convert_hybrid_property_return_type_inner_bool(arg): return Boolean diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index bd8e5f94..808cdbb5 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -2,6 +2,7 @@ import datetime import enum +from decimal import Decimal from typing import List from sqlalchemy import (Column, Date, Enum, ForeignKey, Integer, String, Table, @@ -115,6 +116,10 @@ def hybrid_prop_time(self) -> datetime.time: def hybrid_prop_datetime(self) -> datetime.datetime: return datetime.datetime.now() + @hybrid_property + def hybrid_prop_decimal(self) -> Decimal: + return Decimal("3.14") + @hybrid_property def hybrid_prop_first_article(self) -> Article: return self.articles[0] diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index 90e05b96..fe89d2c4 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -106,6 +106,7 @@ class Meta: "hybrid_prop_date", "hybrid_prop_time", "hybrid_prop_datetime", + "hybrid_prop_decimal", "hybrid_prop_first_article", # Relationship "pets", @@ -174,6 +175,11 @@ class Meta: assert hybrid_prop_datetime.type == DateTime assert hybrid_prop_datetime.description is None # "doc" is ignored by hybrid_property + hybrid_prop_decimal = ReporterType._meta.fields['hybrid_prop_decimal'] + # Decimal (base10) should serialize to String for correctness. + assert hybrid_prop_decimal.type == String + assert hybrid_prop_decimal.description is None # "doc" is ignored by hybrid_property + # relationship favorite_article_field = ReporterType._meta.fields['favorite_article'] assert isinstance(favorite_article_field, Dynamic) @@ -251,6 +257,7 @@ class Meta: "hybrid_prop_date", "hybrid_prop_time", "hybrid_prop_datetime", + "hybrid_prop_decimal", "hybrid_prop_first_article", ]) @@ -367,6 +374,7 @@ class Meta: "hybrid_prop_date", "hybrid_prop_time", "hybrid_prop_datetime", + "hybrid_prop_decimal", "hybrid_prop_first_article", ]) @@ -488,7 +496,7 @@ class Meta: assert issubclass(CustomReporterType, ObjectType) assert CustomReporterType._meta.model == Reporter - assert len(CustomReporterType._meta.fields) == 21 + assert len(CustomReporterType._meta.fields) == 22 # Test Custom SQLAlchemyObjectType with Custom Options From 6f6c61cdf362252c1d4555443c8a886b36d04c23 Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 29 Apr 2022 12:34:44 -0300 Subject: [PATCH 10/19] Fallback to String / Implemented Enum support. --- graphene_sqlalchemy/converter.py | 18 ++++++++++++++++-- graphene_sqlalchemy/utils.py | 10 ++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index b9baed24..874a22bc 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,5 +1,7 @@ import datetime +import enum import typing +import warnings from decimal import Decimal from functools import singledispatch from typing import Any @@ -18,7 +20,8 @@ default_connection_field_factory) from .registry import get_global_registry from .resolvers import get_attr_resolver, get_custom_resolver -from .utils import singledispatchbymatchfunction, value_equals +from .utils import (singledispatchbymatchfunction, value_equals, + value_is_subclass) try: from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, TSVectorType @@ -271,7 +274,13 @@ def convert_hybrid_property_return_type_inner(arg: Any): existing_graphql_type = get_global_registry().get_type_for_model(arg) if existing_graphql_type: return existing_graphql_type - raise Exception(f"I don't know how to generate a GraphQL type out of a \"{arg}\" type") + + # No valid type found, warn and fall back to graphene.String + warnings.warn( + (f"I don't know how to generate a GraphQL type out of a \"{arg}\" type." + "Falling back to \"graphene.String\"") + ) + return String @convert_hybrid_property_return_type_inner.register(value_equals(str)) @@ -317,6 +326,11 @@ def convert_hybrid_property_return_type_inner_time(arg): return Time +@convert_hybrid_property_return_type_inner.register(value_is_subclass(enum.Enum)) +def convert_hybrid_property_return_type_inner_enum(arg): + return Enum.from_enum(arg) + + @convert_hybrid_property_return_type_inner.register(lambda x: getattr(x, '__origin__', None) in [list, typing.List]) def convert_hybrid_property_return_type_inner_list(arg): # type is either list[T] or List[T], generic argument at __args__[0] diff --git a/graphene_sqlalchemy/utils.py b/graphene_sqlalchemy/utils.py index f562ff83..96bb5eed 100644 --- a/graphene_sqlalchemy/utils.py +++ b/graphene_sqlalchemy/utils.py @@ -188,3 +188,13 @@ def value_equals(value): """A simple function that makes the equality based matcher functions for SingleDispatchByMatchFunction prettier""" return lambda x: x == value + + +def value_is_subclass(cls): + def safe_subclass_checker(o): + try: + return issubclass(o, cls) + except TypeError: + return False + + return safe_subclass_checker From 3f3593e2b7b3eb8b4f4dc7f233dfbefcc2686bde Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 29 Apr 2022 12:42:18 -0300 Subject: [PATCH 11/19] better naming conventions (suggested by @erikwrede) --- graphene_sqlalchemy/converter.py | 46 ++++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 874a22bc..c027f07d 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -270,7 +270,7 @@ def convert_json_type_to_string(type, column, registry=None): @singledispatchbymatchfunction -def convert_hybrid_property_return_type_inner(arg: Any): +def convert_sqlalchemy_hybrid_property_type(arg: Any): existing_graphql_type = get_global_registry().get_type_for_model(arg) if existing_graphql_type: return existing_graphql_type @@ -283,60 +283,60 @@ def convert_hybrid_property_return_type_inner(arg: Any): return String -@convert_hybrid_property_return_type_inner.register(value_equals(str)) -def convert_hybrid_property_return_type_inner_str(arg): +@convert_sqlalchemy_hybrid_property_type.register(value_equals(str)) +def convert_sqlalchemy_hybrid_property_type_str(arg): return String -@convert_hybrid_property_return_type_inner.register(value_equals(int)) -def convert_hybrid_property_return_type_inner_int(arg): +@convert_sqlalchemy_hybrid_property_type.register(value_equals(int)) +def convert_sqlalchemy_hybrid_property_type_int(arg): return Int -@convert_hybrid_property_return_type_inner.register(value_equals(float)) -def convert_hybrid_property_return_type_inner_float(arg): +@convert_sqlalchemy_hybrid_property_type.register(value_equals(float)) +def convert_sqlalchemy_hybrid_property_type_float(arg): return Float -@convert_hybrid_property_return_type_inner.register(value_equals(Decimal)) -def convert_hybrid_property_return_type_inner_decimal(arg): +@convert_sqlalchemy_hybrid_property_type.register(value_equals(Decimal)) +def convert_sqlalchemy_hybrid_property_type_decimal(arg): # The reason Decimal should be serialized as a String is because this is a # base10 type used in things like money, and string allows it to not # lose precision (which would happen if we downcasted to a Float, for example) return String -@convert_hybrid_property_return_type_inner.register(value_equals(bool)) -def convert_hybrid_property_return_type_inner_bool(arg): +@convert_sqlalchemy_hybrid_property_type.register(value_equals(bool)) +def convert_sqlalchemy_hybrid_property_type_bool(arg): return Boolean -@convert_hybrid_property_return_type_inner.register(value_equals(datetime.datetime)) -def convert_hybrid_property_return_type_inner_datetime(arg): +@convert_sqlalchemy_hybrid_property_type.register(value_equals(datetime.datetime)) +def convert_sqlalchemy_hybrid_property_type_datetime(arg): return DateTime -@convert_hybrid_property_return_type_inner.register(value_equals(datetime.date)) -def convert_hybrid_property_return_type_inner_date(arg): +@convert_sqlalchemy_hybrid_property_type.register(value_equals(datetime.date)) +def convert_sqlalchemy_hybrid_property_type_date(arg): return Date -@convert_hybrid_property_return_type_inner.register(value_equals(datetime.time)) -def convert_hybrid_property_return_type_inner_time(arg): +@convert_sqlalchemy_hybrid_property_type.register(value_equals(datetime.time)) +def convert_sqlalchemy_hybrid_property_type_time(arg): return Time -@convert_hybrid_property_return_type_inner.register(value_is_subclass(enum.Enum)) -def convert_hybrid_property_return_type_inner_enum(arg): +@convert_sqlalchemy_hybrid_property_type.register(value_is_subclass(enum.Enum)) +def convert_sqlalchemy_hybrid_property_type_enum(arg): return Enum.from_enum(arg) -@convert_hybrid_property_return_type_inner.register(lambda x: getattr(x, '__origin__', None) in [list, typing.List]) -def convert_hybrid_property_return_type_inner_list(arg): +@convert_sqlalchemy_hybrid_property_type.register(lambda x: getattr(x, '__origin__', None) in [list, typing.List]) +def convert_sqlalchemy_hybrid_property_type_list_t(arg): # type is either list[T] or List[T], generic argument at __args__[0] internal_type = arg.__args__[0] - graphql_internal_type = convert_hybrid_property_return_type_inner(internal_type) + graphql_internal_type = convert_sqlalchemy_hybrid_property_type(internal_type) return List(graphql_internal_type) @@ -345,4 +345,4 @@ def convert_hybrid_property_return_type(hybrid_prop): # Grab the original method's return type annotations from inside the hybrid property return_type_annotation = hybrid_prop.fget.__annotations__.get('return', str) - return convert_hybrid_property_return_type_inner(return_type_annotation) + return convert_sqlalchemy_hybrid_property_type(return_type_annotation) From ac6c4bcc7f3b773c5d03c26080a0dae536655e47 Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 29 Apr 2022 12:53:28 -0300 Subject: [PATCH 12/19] Reverted all tests. We will be writing new tests specifically for the hybrid_property type inference thing so that we don't need to pepper unwanted imports and generations in other unrelated tests. --- graphene_sqlalchemy/tests/test_batching.py | 247 +++++++++--------- graphene_sqlalchemy/tests/test_benchmark.py | 28 +- graphene_sqlalchemy/tests/test_converter.py | 11 - graphene_sqlalchemy/tests/test_query.py | 48 ++-- graphene_sqlalchemy/tests/test_query_enums.py | 8 +- graphene_sqlalchemy/tests/test_types.py | 225 ++++++---------- 6 files changed, 237 insertions(+), 330 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_batching.py b/graphene_sqlalchemy/tests/test_batching.py index 12879afe..1896900b 100644 --- a/graphene_sqlalchemy/tests/test_batching.py +++ b/graphene_sqlalchemy/tests/test_batching.py @@ -17,7 +17,6 @@ class MockLoggingHandler(logging.Handler): """Intercept and store log messages in a list.""" - def __init__(self, *args, **kwargs): self.messages = [] logging.Handler.__init__(self, *args, **kwargs) @@ -43,15 +42,15 @@ def mock_sqlalchemy_logging_handler(): def get_schema(): - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (relay.Node,) batching = True - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (relay.Node,) batching = True @@ -83,11 +82,11 @@ async def test_many_to_one(session_factory): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -139,20 +138,20 @@ async def test_many_to_one(session_factory): assert not result.errors result = to_std_dicts(result.data) assert result == { - "articles": [ - { - "headline": "Article_1", - "reporter": { - "firstName": "Reporter_1", - }, - }, - { - "headline": "Article_2", - "reporter": { - "firstName": "Reporter_2", - }, - }, - ], + "articles": [ + { + "headline": "Article_1", + "reporter": { + "firstName": "Reporter_1", + }, + }, + { + "headline": "Article_2", + "reporter": { + "firstName": "Reporter_2", + }, + }, + ], } @@ -161,11 +160,11 @@ async def test_one_to_one(session_factory): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -217,20 +216,20 @@ async def test_one_to_one(session_factory): assert not result.errors result = to_std_dicts(result.data) assert result == { - "reporters": [ - { - "firstName": "Reporter_1", - "favoriteArticle": { - "headline": "Article_1", - }, - }, - { - "firstName": "Reporter_2", - "favoriteArticle": { - "headline": "Article_2", - }, - }, - ], + "reporters": [ + { + "firstName": "Reporter_1", + "favoriteArticle": { + "headline": "Article_1", + }, + }, + { + "firstName": "Reporter_2", + "favoriteArticle": { + "headline": "Article_2", + }, + }, + ], } @@ -239,11 +238,11 @@ async def test_one_to_many(session_factory): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -307,42 +306,42 @@ async def test_one_to_many(session_factory): assert not result.errors result = to_std_dicts(result.data) assert result == { - "reporters": [ - { - "firstName": "Reporter_1", - "articles": { - "edges": [ - { - "node": { - "headline": "Article_1", - }, - }, - { - "node": { - "headline": "Article_2", - }, - }, - ], + "reporters": [ + { + "firstName": "Reporter_1", + "articles": { + "edges": [ + { + "node": { + "headline": "Article_1", + }, + }, + { + "node": { + "headline": "Article_2", + }, + }, + ], + }, + }, + { + "firstName": "Reporter_2", + "articles": { + "edges": [ + { + "node": { + "headline": "Article_3", }, - }, - { - "firstName": "Reporter_2", - "articles": { - "edges": [ - { - "node": { - "headline": "Article_3", - }, - }, - { - "node": { - "headline": "Article_4", - }, - }, - ], + }, + { + "node": { + "headline": "Article_4", }, - }, - ], + }, + ], + }, + }, + ], } @@ -351,11 +350,11 @@ async def test_many_to_many(session_factory): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -421,42 +420,42 @@ async def test_many_to_many(session_factory): assert not result.errors result = to_std_dicts(result.data) assert result == { - "reporters": [ - { - "firstName": "Reporter_1", - "pets": { - "edges": [ - { - "node": { - "name": "Pet_1", - }, - }, - { - "node": { - "name": "Pet_2", - }, - }, - ], + "reporters": [ + { + "firstName": "Reporter_1", + "pets": { + "edges": [ + { + "node": { + "name": "Pet_1", }, - }, - { - "firstName": "Reporter_2", - "pets": { - "edges": [ - { - "node": { - "name": "Pet_3", - }, - }, - { - "node": { - "name": "Pet_4", - }, - }, - ], + }, + { + "node": { + "name": "Pet_2", }, - }, - ], + }, + ], + }, + }, + { + "firstName": "Reporter_2", + "pets": { + "edges": [ + { + "node": { + "name": "Pet_3", + }, + }, + { + "node": { + "name": "Pet_4", + }, + }, + ], + }, + }, + ], } @@ -469,11 +468,6 @@ def test_disable_batching_via_ormfield(session_factory): session.commit() session.close() - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (relay.Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -483,6 +477,11 @@ class Meta: favorite_article = ORMField(batching=False) articles = ORMField(batching=False) + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (relay.Node,) + class Query(graphene.ObjectType): reporters = graphene.Field(graphene.List(ReporterType)) @@ -542,11 +541,6 @@ async def test_connection_factory_field_overrides_batching_is_false(session_fact session.commit() session.close() - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (relay.Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -556,6 +550,11 @@ class Meta: articles = ORMField(batching=False) + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (relay.Node,) + class Query(graphene.ObjectType): reporters = graphene.Field(graphene.List(ReporterType)) @@ -601,11 +600,6 @@ def test_connection_factory_field_overrides_batching_is_true(session_factory): session.commit() session.close() - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (relay.Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -615,6 +609,11 @@ class Meta: articles = ORMField(batching=True) + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (relay.Node,) + class Query(graphene.ObjectType): reporters = graphene.Field(graphene.List(ReporterType)) diff --git a/graphene_sqlalchemy/tests/test_benchmark.py b/graphene_sqlalchemy/tests/test_benchmark.py index 834b7f04..11e9d0e0 100644 --- a/graphene_sqlalchemy/tests/test_benchmark.py +++ b/graphene_sqlalchemy/tests/test_benchmark.py @@ -12,14 +12,14 @@ def get_schema(): - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (relay.Node,) - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (relay.Node,) class PetType(SQLAlchemyObjectType): @@ -46,8 +46,8 @@ def benchmark_query(session_factory, benchmark, query): @benchmark def execute_query(): result = schema.execute( - query, - context_value={"session": session_factory()}, + query, + context_value={"session": session_factory()}, ) assert not result.errors @@ -56,11 +56,11 @@ def test_one_to_one(session_factory, benchmark): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -91,11 +91,11 @@ def test_many_to_one(session_factory, benchmark): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -126,11 +126,11 @@ def test_one_to_many(session_factory, benchmark): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) @@ -173,11 +173,11 @@ def test_many_to_many(session_factory, benchmark): session = session_factory() reporter_1 = Reporter( - first_name='Reporter_1', + first_name='Reporter_1', ) session.add(reporter_1) reporter_2 = Reporter( - first_name='Reporter_2', + first_name='Reporter_2', ) session.add(reporter_2) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 0809312e..57c43058 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -9,7 +9,6 @@ from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType import graphene -from graphene import relay from graphene.relay import Node from graphene.types.datetime import DateTime from graphene.types.json import JSONString @@ -244,11 +243,6 @@ class Meta: def test_should_manytoone_convert_connectionorlist_list(): - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (relay.Node,) - class A(SQLAlchemyObjectType): class Meta: model = Reporter @@ -263,11 +257,6 @@ class Meta: def test_should_manytoone_convert_connectionorlist_connection(): - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (relay.Node,) - class A(SQLAlchemyObjectType): class Meta: model = Reporter diff --git a/graphene_sqlalchemy/tests/test_query.py b/graphene_sqlalchemy/tests/test_query.py index f6b673cf..39140814 100644 --- a/graphene_sqlalchemy/tests/test_query.py +++ b/graphene_sqlalchemy/tests/test_query.py @@ -1,5 +1,4 @@ import graphene -from graphene import relay from graphene.relay import Node from ..converter import convert_sqlalchemy_composite @@ -37,11 +36,6 @@ def test_query_fields(session): def convert_composite_class(composite, registry): return graphene.String() - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (relay.Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -61,7 +55,7 @@ def resolve_reporters(self, _info): reporter { firstName columnProp - hybridPropUntyped + hybridProp compositeProp } reporters { @@ -72,7 +66,7 @@ def resolve_reporters(self, _info): expected = { "reporter": { "firstName": "John", - "hybridPropUntyped": "John", + "hybridProp": "John", "columnProp": 2, "compositeProp": "John Doe", }, @@ -88,11 +82,6 @@ def resolve_reporters(self, _info): def test_query_node(session): add_test_data(session) - class ArticleNode(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterNode(SQLAlchemyObjectType): class Meta: model = Reporter @@ -102,6 +91,11 @@ class Meta: def get_node(cls, info, id): return Reporter(id=2, first_name="Cookie Monster") + class ArticleNode(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class Query(graphene.ObjectType): node = Node.Field() reporter = graphene.Field(ReporterNode) @@ -164,23 +158,23 @@ def test_orm_field(session): def convert_composite_class(composite, registry): return graphene.String() - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter interfaces = (Node,) first_name_v2 = ORMField(model_attr='first_name') - hybrid_prop_untyped_v2 = ORMField(model_attr='hybrid_prop_untyped') + hybrid_prop_v2 = ORMField(model_attr='hybrid_prop') column_prop_v2 = ORMField(model_attr='column_prop') composite_prop = ORMField() favorite_article_v2 = ORMField(model_attr='favorite_article') articles_v2 = ORMField(model_attr='articles') + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) @@ -191,13 +185,13 @@ def resolve_reporter(self, _info): query { reporter { firstNameV2 - hybridPropUntypedV2 + hybridPropV2 columnPropV2 compositeProp favoriteArticleV2 { headline } - articlesV2 { + articlesV2(first: 1) { edges { node { headline @@ -210,7 +204,7 @@ def resolve_reporter(self, _info): expected = { "reporter": { "firstNameV2": "John", - "hybridPropUntypedV2": "John", + "hybridPropV2": "John", "columnPropV2": 2, "compositeProp": "John Doe", "favoriteArticleV2": {"headline": "Hi!"}, @@ -268,11 +262,6 @@ class Query(graphene.ObjectType): def test_mutation(session): add_test_data(session) - class ArticleNode(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class EditorNode(SQLAlchemyObjectType): class Meta: model = Editor @@ -287,6 +276,11 @@ class Meta: def get_node(cls, id, info): return Reporter(id=2, first_name="Cookie Monster") + class ArticleNode(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class CreateArticle(graphene.Mutation): class Arguments: headline = graphene.String() diff --git a/graphene_sqlalchemy/tests/test_query_enums.py b/graphene_sqlalchemy/tests/test_query_enums.py index 07c540cb..5166c45f 100644 --- a/graphene_sqlalchemy/tests/test_query_enums.py +++ b/graphene_sqlalchemy/tests/test_query_enums.py @@ -1,8 +1,7 @@ import graphene -from graphene import Node from ..types import SQLAlchemyObjectType -from .models import Article, HairKind, Pet, Reporter +from .models import HairKind, Pet, Reporter from .test_query import add_test_data, to_std_dicts @@ -14,11 +13,6 @@ class PetType(SQLAlchemyObjectType): class Meta: model = Pet - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index fe89d2c4..4601b65d 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -4,9 +4,8 @@ import sqlalchemy.exc import sqlalchemy.orm.exc -from graphene import (Boolean, Date, DateTime, Dynamic, Field, Float, GlobalID, - Int, List, Node, NonNull, ObjectType, Schema, String, - Time) +from graphene import (Boolean, Dynamic, Field, Float, GlobalID, Int, List, + Node, NonNull, ObjectType, Schema, String) from graphene.relay import Connection from .. import utils @@ -35,11 +34,6 @@ class Meta: def test_sqlalchemy_node(session): - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -57,11 +51,6 @@ class Meta: def test_connection(): - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -75,17 +64,17 @@ def test_sqlalchemy_default_fields(): def convert_composite_class(composite, registry): return String() - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (Node,) - assert sorted(list(ReporterType._meta.fields.keys())) == sorted([ + assert list(ReporterType._meta.fields.keys()) == [ # Columns "column_prop", # SQLAlchemy retuns column properties first "id", @@ -96,89 +85,70 @@ class Meta: # Composite "composite_prop", # Hybrid - "hybrid_prop_untyped", + "hybrid_prop", "hybrid_prop_str", "hybrid_prop_int", "hybrid_prop_float", "hybrid_prop_bool", - "hybrid_prop_list_int", - "hybrid_prop_list_date", - "hybrid_prop_date", - "hybrid_prop_time", - "hybrid_prop_datetime", - "hybrid_prop_decimal", - "hybrid_prop_first_article", + "hybrid_prop_list", # Relationship "pets", "articles", "favorite_article", - ]) - - # Check field types and descriptions + ] + # column first_name_field = ReporterType._meta.fields['first_name'] assert first_name_field.type == String assert first_name_field.description == "First name" - # Column Property + # column_property column_prop_field = ReporterType._meta.fields['column_prop'] assert column_prop_field.type == Int - assert column_prop_field.description is None # "doc" is ignored by column_property + # "doc" is ignored by column_property + assert column_prop_field.description is None - # Composite Property + # composite full_name_field = ReporterType._meta.fields['composite_prop'] assert full_name_field.type == String - assert full_name_field.description is None # "doc" is ignored by composite - - # Hybrid Property: Untyped Return (fallback to String) - hybrid_prop_untyped = ReporterType._meta.fields['hybrid_prop_untyped'] - assert hybrid_prop_untyped.type == String - assert hybrid_prop_untyped.description is None # "doc" is ignored by hybrid_property + # "doc" is ignored by composite + assert full_name_field.description is None - ################################################# - # Hybrid Properties with return type annotations - ################################################# + # hybrid_property + hybrid_prop = ReporterType._meta.fields['hybrid_prop'] + assert hybrid_prop.type == String + # "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 - assert hybrid_prop_str.description is None # "doc" is ignored by hybrid_property + # "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 - assert hybrid_prop_int.description is None # "doc" is ignored by hybrid_property + # "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 - assert hybrid_prop_float.description is None # "doc" is ignored by hybrid_property + # "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 - assert hybrid_prop_bool.description is None # "doc" is ignored by hybrid_property + # "doc" is ignored by hybrid_property + assert hybrid_prop_bool.description is None - hybrid_prop_list_int = ReporterType._meta.fields['hybrid_prop_list_int'] - assert hybrid_prop_list_int.type == List(Int) - assert hybrid_prop_list_int.description is None # "doc" is ignored by hybrid_property - - hybrid_prop_list_date = ReporterType._meta.fields['hybrid_prop_list_date'] - assert hybrid_prop_list_date.type == List(Date) - assert hybrid_prop_list_date.description is None # "doc" is ignored by hybrid_property - - hybrid_prop_date = ReporterType._meta.fields['hybrid_prop_date'] - assert hybrid_prop_date.type == Date - assert hybrid_prop_date.description is None # "doc" is ignored by hybrid_property - - hybrid_prop_time = ReporterType._meta.fields['hybrid_prop_time'] - assert hybrid_prop_time.type == Time - assert hybrid_prop_time.description is None # "doc" is ignored by hybrid_property - - hybrid_prop_datetime = ReporterType._meta.fields['hybrid_prop_datetime'] - assert hybrid_prop_datetime.type == DateTime - assert hybrid_prop_datetime.description is None # "doc" is ignored by hybrid_property - - hybrid_prop_decimal = ReporterType._meta.fields['hybrid_prop_decimal'] - # Decimal (base10) should serialize to String for correctness. - assert hybrid_prop_decimal.type == String - assert hybrid_prop_decimal.description is None # "doc" is ignored by hybrid_property + # 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'] @@ -197,11 +167,6 @@ class ReporterMixin(object): first_name = ORMField(required=True) last_name = ORMField(description='Overridden') - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType, ReporterMixin): class Meta: model = Reporter @@ -218,20 +183,25 @@ class Meta: composite_prop = ORMField() # hybrid_property - hybrid_prop_untyped = ORMField(description='Overridden') + hybrid_prop = ORMField(description='Overridden') # relationships favorite_article = ORMField(description='Overridden') articles = ORMField(deprecation_reason='Overridden') pets = ORMField(description='Overridden') + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + class PetType(SQLAlchemyObjectType): class Meta: model = Pet interfaces = (Node,) use_connection = False - assert sorted(list(ReporterType._meta.fields.keys())) == sorted([ + assert list(ReporterType._meta.fields.keys()) == [ # Fields from ReporterMixin "first_name", "last_name", @@ -240,26 +210,19 @@ class Meta: "email_v2", "column_prop", "composite_prop", + "hybrid_prop", "favorite_article", "articles", "pets", # Then the automatic SQLAlchemy fields "id", "favorite_pet_kind", - # Hybrid - "hybrid_prop_untyped", "hybrid_prop_str", "hybrid_prop_int", "hybrid_prop_float", "hybrid_prop_bool", - "hybrid_prop_list_int", - "hybrid_prop_list_date", - "hybrid_prop_date", - "hybrid_prop_time", - "hybrid_prop_datetime", - "hybrid_prop_decimal", - "hybrid_prop_first_article", - ]) + "hybrid_prop_list", + ] first_name_field = ReporterType._meta.fields['first_name'] assert isinstance(first_name_field.type, NonNull) @@ -282,10 +245,10 @@ class Meta: assert email_field_v2.description == "Email" assert email_field_v2.deprecation_reason is None - hybrid_prop_untyped_field = ReporterType._meta.fields['hybrid_prop_untyped'] - assert hybrid_prop_untyped_field.type == String - assert hybrid_prop_untyped_field.description == "Overridden" - assert hybrid_prop_untyped_field.deprecation_reason is None + hybrid_prop_field = ReporterType._meta.fields['hybrid_prop'] + assert hybrid_prop_field.type == String + assert hybrid_prop_field.description == "Overridden" + assert hybrid_prop_field.deprecation_reason is None column_prop_field_v2 = ReporterType._meta.fields['column_prop'] assert column_prop_field_v2.type == String @@ -340,11 +303,6 @@ class Meta: def test_exclude_fields(): - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -353,30 +311,23 @@ class Meta: first_name = ORMField() # Takes precedence last_name = ORMField() # Noop - assert sorted(list(ReporterType._meta.fields.keys())) == sorted([ + assert list(ReporterType._meta.fields.keys()) == [ "first_name", "last_name", "column_prop", "email", "favorite_pet_kind", "composite_prop", - "pets", - "articles", - "favorite_article", - # Hybrid - "hybrid_prop_untyped", + "hybrid_prop", "hybrid_prop_str", "hybrid_prop_int", "hybrid_prop_float", "hybrid_prop_bool", - "hybrid_prop_list_int", - "hybrid_prop_list_date", - "hybrid_prop_date", - "hybrid_prop_time", - "hybrid_prop_datetime", - "hybrid_prop_decimal", - "hybrid_prop_first_article", - ]) + "hybrid_prop_list", + "pets", + "articles", + "favorite_article", + ] def test_only_and_exclude_fields(): @@ -390,11 +341,6 @@ class Meta: def test_sqlalchemy_redefine_field(): - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter @@ -409,11 +355,6 @@ class Meta: def test_resolvers(session): """Test that the correct resolver functions are called""" - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterMixin(object): def resolve_id(root, _info): return 'ID' @@ -481,11 +422,6 @@ def resolve_reporter(self, _info): # Test Custom SQLAlchemyObjectType Implementation def test_custom_objecttype_registered(): - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class CustomSQLAlchemyObjectType(SQLAlchemyObjectType): class Meta: abstract = True @@ -496,7 +432,7 @@ class Meta: assert issubclass(CustomReporterType, ObjectType) assert CustomReporterType._meta.model == Reporter - assert len(CustomReporterType._meta.fields) == 22 + assert len(CustomReporterType._meta.fields) == 16 # Test Custom SQLAlchemyObjectType with Custom Options @@ -516,11 +452,6 @@ def __init_subclass_with_meta__(cls, custom_option=None, **options): _meta=_meta, **options ) - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterWithCustomOptions(SQLAlchemyObjectTypeWithCustomOptions): class Meta: model = Reporter @@ -538,14 +469,14 @@ class _TestSQLAlchemyConnectionField(SQLAlchemyConnectionField): def test_default_connection_field_factory(): - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (Node,) assert isinstance(ReporterType._meta.fields['articles'].type(), UnsortedSQLAlchemyConnectionField) @@ -557,17 +488,17 @@ def test_connection_field_factory(relationship, registry): _type = registry.get_type_for_model(model) return _TestSQLAlchemyConnectionField(_type._meta.connection) - class ArticleType(SQLAlchemyObjectType): - class Meta: - model = Article - interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter interfaces = (Node,) connection_field_factory = test_connection_field_factory + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + interfaces = (Node,) + assert isinstance(ReporterType._meta.fields['articles'].type(), _TestSQLAlchemyConnectionField) @@ -575,14 +506,14 @@ def test_deprecated_registerConnectionFieldFactory(): with pytest.warns(DeprecationWarning): registerConnectionFieldFactory(_TestSQLAlchemyConnectionField) - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (Node,) assert isinstance(ReporterType._meta.fields['articles'].type(), _TestSQLAlchemyConnectionField) @@ -593,14 +524,14 @@ def test_deprecated_unregisterConnectionFieldFactory(): registerConnectionFieldFactory(_TestSQLAlchemyConnectionField) unregisterConnectionFieldFactory() - class ArticleType(SQLAlchemyObjectType): + class ReporterType(SQLAlchemyObjectType): class Meta: - model = Article + model = Reporter interfaces = (Node,) - class ReporterType(SQLAlchemyObjectType): + class ArticleType(SQLAlchemyObjectType): class Meta: - model = Reporter + model = Article interfaces = (Node,) assert not isinstance(ReporterType._meta.fields['articles'].type(), _TestSQLAlchemyConnectionField) From 44f0e62c97a8af51c147bd09d2ff611d848767af Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 29 Apr 2022 14:41:31 -0300 Subject: [PATCH 13/19] removed unused function --- graphene_sqlalchemy/utils.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/graphene_sqlalchemy/utils.py b/graphene_sqlalchemy/utils.py index 96bb5eed..f562ff83 100644 --- a/graphene_sqlalchemy/utils.py +++ b/graphene_sqlalchemy/utils.py @@ -188,13 +188,3 @@ def value_equals(value): """A simple function that makes the equality based matcher functions for SingleDispatchByMatchFunction prettier""" return lambda x: x == value - - -def value_is_subclass(cls): - def safe_subclass_checker(o): - try: - return issubclass(o, cls) - except TypeError: - return False - - return safe_subclass_checker From 95323f57b3d2c5cd115b94128ccad4f6a50d4006 Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 29 Apr 2022 15:26:57 -0300 Subject: [PATCH 14/19] Tests: Fix failing tests and ensure **stable ordering** of property checks via sorted() --- graphene_sqlalchemy/tests/test_types.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index 4601b65d..2d660b67 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -74,7 +74,7 @@ class Meta: model = Article interfaces = (Node,) - assert list(ReporterType._meta.fields.keys()) == [ + assert sorted(list(ReporterType._meta.fields.keys())) == sorted([ # Columns "column_prop", # SQLAlchemy retuns column properties first "id", @@ -95,7 +95,7 @@ class Meta: "pets", "articles", "favorite_article", - ] + ]) # column first_name_field = ReporterType._meta.fields['first_name'] @@ -201,7 +201,7 @@ class Meta: interfaces = (Node,) use_connection = False - assert list(ReporterType._meta.fields.keys()) == [ + assert sorted(list(ReporterType._meta.fields.keys())) == sorted([ # Fields from ReporterMixin "first_name", "last_name", @@ -222,7 +222,7 @@ class Meta: "hybrid_prop_float", "hybrid_prop_bool", "hybrid_prop_list", - ] + ]) first_name_field = ReporterType._meta.fields['first_name'] assert isinstance(first_name_field.type, NonNull) @@ -311,7 +311,7 @@ class Meta: first_name = ORMField() # Takes precedence last_name = ORMField() # Noop - assert list(ReporterType._meta.fields.keys()) == [ + assert sorted(list(ReporterType._meta.fields.keys())) == sorted([ "first_name", "last_name", "column_prop", @@ -327,7 +327,7 @@ class Meta: "pets", "articles", "favorite_article", - ] + ]) def test_only_and_exclude_fields(): From c57eadabcba3245d135565c31733af062183c7bc Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 29 Apr 2022 15:27:54 -0300 Subject: [PATCH 15/19] New test and models specific to hybrid_property type inference tests. --- graphene_sqlalchemy/converter.py | 9 +- graphene_sqlalchemy/tests/models.py | 102 +++++++++++++++----- graphene_sqlalchemy/tests/test_converter.py | 57 ++++++++++- 3 files changed, 135 insertions(+), 33 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index c027f07d..d031b13f 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,5 +1,4 @@ import datetime -import enum import typing import warnings from decimal import Decimal @@ -20,8 +19,7 @@ default_connection_field_factory) from .registry import get_global_registry from .resolvers import get_attr_resolver, get_custom_resolver -from .utils import (singledispatchbymatchfunction, value_equals, - value_is_subclass) +from .utils import singledispatchbymatchfunction, value_equals try: from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, TSVectorType @@ -326,11 +324,6 @@ def convert_sqlalchemy_hybrid_property_type_time(arg): return Time -@convert_sqlalchemy_hybrid_property_type.register(value_is_subclass(enum.Enum)) -def convert_sqlalchemy_hybrid_property_type_enum(arg): - return Enum.from_enum(arg) - - @convert_sqlalchemy_hybrid_property_type.register(lambda x: getattr(x, '__origin__', None) in [list, typing.List]) def convert_sqlalchemy_hybrid_property_type_list_t(arg): # type is either list[T] or List[T], generic argument at __args__[0] diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index 808cdbb5..ea112747 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -77,7 +77,7 @@ class Reporter(Base): favorite_article = relationship("Article", uselist=False) @hybrid_property - def hybrid_prop_untyped(self): + def hybrid_prop(self): return self.first_name @hybrid_property @@ -97,48 +97,104 @@ def hybrid_prop_bool(self) -> bool: return True @hybrid_property - def hybrid_prop_list_int(self) -> List[int]: + 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" + ) + + composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite") + + +class ReflectedEditor(type): + """Same as Editor, but using reflected table.""" + + @classmethod + def __subclasses__(cls): + return [] + + +editor_table = Table("editors", Base.metadata, autoload=True) + +mapper(ReflectedEditor, editor_table) + + +############################################ +# The models below are mainly used in the +# @hybrid_property type inference scenarios +############################################ + + +class ShoppingCartItem(Base): + __tablename__ = "shopping_cart_items" + + id = Column(Integer(), primary_key=True) + + +class ShoppingCart(Base): + __tablename__ = "shopping_carts" + + id = Column(Integer(), primary_key=True) + + # Standard Library types + @hybrid_property - def hybrid_prop_list_date(self) -> List[datetime.date]: - return [self.hybrid_prop_date, self.hybrid_prop_date, self.hybrid_prop_date] + def hybrid_prop_str(self) -> str: + return self.first_name @hybrid_property - def hybrid_prop_date(self) -> datetime.date: - return datetime.datetime.now().date() + def hybrid_prop_int(self) -> int: + return 42 @hybrid_property - def hybrid_prop_time(self) -> datetime.time: - return datetime.datetime.now().time() + def hybrid_prop_float(self) -> float: + return 42.3 @hybrid_property - def hybrid_prop_datetime(self) -> datetime.datetime: - return datetime.datetime.now() + def hybrid_prop_bool(self) -> bool: + return True @hybrid_property def hybrid_prop_decimal(self) -> Decimal: return Decimal("3.14") @hybrid_property - def hybrid_prop_first_article(self) -> Article: - return self.articles[0] + def hybrid_prop_date(self) -> datetime.date: + return datetime.datetime.now().date() - column_prop = column_property( - select([func.cast(func.count(id), Integer)]), doc="Column property" - ) + @hybrid_property + def hybrid_prop_time(self) -> datetime.time: + return datetime.datetime.now().time() - composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite") + @hybrid_property + def hybrid_prop_datetime(self) -> datetime.datetime: + return datetime.datetime.now() + # Lists and Nested Lists -class ReflectedEditor(type): - """Same as Editor, but using reflected table.""" + @hybrid_property + def hybrid_prop_list_int(self) -> List[int]: + return [1, 2, 3] - @classmethod - def __subclasses__(cls): - return [] + @hybrid_property + def hybrid_prop_list_date(self) -> List[datetime.date]: + return [self.hybrid_prop_date, self.hybrid_prop_date, self.hybrid_prop_date] + @hybrid_property + def hybrid_prop_nested_list_int(self) -> List[List[int]]: + return [self.hybrid_prop_list_int, ] -editor_table = Table("editors", Base.metadata, autoload=True) + @hybrid_property + def hybrid_prop_deeply_nested_list_int(self) -> List[List[List[int]]]: + return [[self.hybrid_prop_list_int, ], ] -mapper(ReflectedEditor, editor_table) + # Other SQLAlchemy Instances + @hybrid_property + def hybrid_prop_first_shopping_cart_item(self) -> ShoppingCartItem: + return ShoppingCartItem(id=1) + + # Other SQLAlchemy Instances + @hybrid_property + def hybrid_prop_shopping_cart_item_list(self) -> List[ShoppingCartItem]: + return [ShoppingCartItem(id=1), ShoppingCartItem(id=2)] diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 57c43058..ebf7f8d2 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -1,4 +1,5 @@ import enum +from typing import Dict, Union import pytest from sqlalchemy import Column, func, select, types @@ -9,9 +10,11 @@ from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType import graphene +from graphene import Boolean, Float, Int, Scalar, String from graphene.relay import Node -from graphene.types.datetime import DateTime +from graphene.types.datetime import Date, DateTime, Time from graphene.types.json import JSONString +from graphene.types.structures import List, Structure from ..converter import (convert_sqlalchemy_column, convert_sqlalchemy_composite, @@ -20,7 +23,8 @@ default_connection_field_factory) from ..registry import Registry, get_global_registry from ..types import SQLAlchemyObjectType -from .models import Article, CompositeFullName, Pet, Reporter +from .models import (Article, CompositeFullName, Pet, Reporter, ShoppingCart, + ShoppingCartItem) def mock_resolver(): @@ -384,3 +388,52 @@ def __init__(self, col1, col2): Registry(), mock_resolver, ) + + +def test_sqlalchemy_hybrid_property_type_inference(): + class ShoppingCartItemType(SQLAlchemyObjectType): + class Meta: + model = ShoppingCartItem + interfaces = (Node,) + + class ShoppingCartType(SQLAlchemyObjectType): + class Meta: + model = ShoppingCart + interfaces = (Node,) + + hybrid_prop_expected_types: Dict[str, Union[Scalar, Structure]] = { + # Basic types + "hybrid_prop_str": String, + "hybrid_prop_int": Int, + "hybrid_prop_float": Float, + "hybrid_prop_bool": Boolean, + "hybrid_prop_decimal": String, # Decimals should be serialized Strings + "hybrid_prop_date": Date, + "hybrid_prop_time": Time, + "hybrid_prop_datetime": DateTime, + # Lists and Nested Lists + "hybrid_prop_list_int": List(Int), + "hybrid_prop_list_date": List(Date), + "hybrid_prop_nested_list_int": List(List(Int)), + "hybrid_prop_deeply_nested_list_int": List(List(List(Int))), + "hybrid_prop_first_shopping_cart_item": ShoppingCartItemType, + "hybrid_prop_shopping_cart_item_list": List(ShoppingCartItemType) + } + + assert sorted(list(ShoppingCartType._meta.fields.keys())) == sorted([ + # Columns + "id", + # Append Hybrid Properties from Above + *hybrid_prop_expected_types.keys() + ]) + + for hybrid_prop_name, hybrid_prop_expected_return_type in hybrid_prop_expected_types.items(): + hybrid_prop_field = ShoppingCartType._meta.fields[hybrid_prop_name] + + # this is a simple way of showing the failed property name + # instead of having to unroll the loop. + assert ( + (hybrid_prop_name, str(hybrid_prop_field.type)) == + (hybrid_prop_name, str(hybrid_prop_expected_return_type)) + ) + assert hybrid_prop_field.description is None # "doc" is ignored by hybrid property From 6c533515ba40e1d11af1a84f375875bda388938c Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 29 Apr 2022 15:31:49 -0300 Subject: [PATCH 16/19] Add a test for an unsupported type (Tuple), and test it coerces back to String. --- graphene_sqlalchemy/tests/models.py | 8 +++++++- graphene_sqlalchemy/tests/test_converter.py | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index ea112747..d1a6f1ea 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -3,7 +3,7 @@ import datetime import enum from decimal import Decimal -from typing import List +from typing import List, Tuple from sqlalchemy import (Column, Date, Enum, ForeignKey, Integer, String, Table, func, select) @@ -198,3 +198,9 @@ def hybrid_prop_first_shopping_cart_item(self) -> ShoppingCartItem: @hybrid_property def hybrid_prop_shopping_cart_item_list(self) -> List[ShoppingCartItem]: return [ShoppingCartItem(id=1), ShoppingCartItem(id=2)] + + # Unsupported Type + + @hybrid_property + def hybrid_prop_unsupported_type_tuple(self) -> Tuple[str, str]: + return "this will actually", "be a string" diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index ebf7f8d2..d1987326 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -417,7 +417,8 @@ class Meta: "hybrid_prop_nested_list_int": List(List(Int)), "hybrid_prop_deeply_nested_list_int": List(List(List(Int))), "hybrid_prop_first_shopping_cart_item": ShoppingCartItemType, - "hybrid_prop_shopping_cart_item_list": List(ShoppingCartItemType) + "hybrid_prop_shopping_cart_item_list": List(ShoppingCartItemType), + "hybrid_prop_unsupported_type_tuple": String, } assert sorted(list(ShoppingCartType._meta.fields.keys())) == sorted([ From a17782b96e05d903f78fcba4bac4467828ece17d Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 29 Apr 2022 15:40:00 -0300 Subject: [PATCH 17/19] Lint. --- graphene_sqlalchemy/tests/models.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index d1a6f1ea..b6eb4faf 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -56,14 +56,6 @@ def __repr__(self): return "{} {}".format(self.first_name, self.last_name) -class Article(Base): - __tablename__ = "articles" - id = Column(Integer(), primary_key=True) - headline = Column(String(100)) - pub_date = Column(Date()) - reporter_id = Column(Integer(), ForeignKey("reporters.id")) - - class Reporter(Base): __tablename__ = "reporters" @@ -107,6 +99,14 @@ def hybrid_prop_list(self) -> List[int]: composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite") +class Article(Base): + __tablename__ = "articles" + id = Column(Integer(), primary_key=True) + headline = Column(String(100)) + pub_date = Column(Date()) + reporter_id = Column(Integer(), ForeignKey("reporters.id")) + + class ReflectedEditor(type): """Same as Editor, but using reflected table.""" @@ -200,7 +200,6 @@ def hybrid_prop_shopping_cart_item_list(self) -> List[ShoppingCartItem]: return [ShoppingCartItem(id=1), ShoppingCartItem(id=2)] # Unsupported Type - @hybrid_property def hybrid_prop_unsupported_type_tuple(self) -> Tuple[str, str]: return "this will actually", "be a string" From 687e96e3c98c5e1dd5c2f2f7b1ff9c18f4830173 Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 29 Apr 2022 17:56:47 -0300 Subject: [PATCH 18/19] Self-referential SQLAlchemy Model Support. --- graphene_sqlalchemy/converter.py | 35 ++++++++++++++++++++- graphene_sqlalchemy/tests/models.py | 10 ++++++ graphene_sqlalchemy/tests/test_converter.py | 3 ++ graphene_sqlalchemy/utils.py | 20 +++++++++++- 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index d031b13f..a2e03694 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -19,7 +19,14 @@ default_connection_field_factory) from .registry import get_global_registry from .resolvers import get_attr_resolver, get_custom_resolver -from .utils import singledispatchbymatchfunction, value_equals +from .utils import (registry_sqlalchemy_model_from_str, safe_isinstance, + singledispatchbymatchfunction, value_equals) + +try: + from typing import ForwardRef +except ImportError: + # python 3.6 + from typing import _ForwardRef as ForwardRef try: from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, TSVectorType @@ -334,6 +341,32 @@ def convert_sqlalchemy_hybrid_property_type_list_t(arg): return List(graphql_internal_type) +@convert_sqlalchemy_hybrid_property_type.register(safe_isinstance(ForwardRef)) +def convert_sqlalchemy_hybrid_property_forwardref(arg): + """ + Generate a lambda that will resolve the type at runtime + This takes care of self-references + """ + + def forward_reference_solver(): + model = registry_sqlalchemy_model_from_str(arg.__forward_arg__) + if not model: + return String + # Always fall back to string if no ForwardRef type found. + return get_global_registry().get_type_for_model(model) + + return forward_reference_solver + + +@convert_sqlalchemy_hybrid_property_type.register(safe_isinstance(str)) +def convert_sqlalchemy_hybrid_property_bare_str(arg): + """ + Convert Bare String into a ForwardRef + """ + + return convert_sqlalchemy_hybrid_property_type(ForwardRef(arg)) + + def convert_hybrid_property_return_type(hybrid_prop): # Grab the original method's return type annotations from inside the hybrid property return_type_annotation = hybrid_prop.fget.__annotations__.get('return', str) diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index b6eb4faf..0a519882 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -203,3 +203,13 @@ def hybrid_prop_shopping_cart_item_list(self) -> List[ShoppingCartItem]: @hybrid_property def hybrid_prop_unsupported_type_tuple(self) -> Tuple[str, str]: return "this will actually", "be a string" + + # Self-references + + @hybrid_property + def hybrid_prop_self_referential(self) -> 'ShoppingCart': + return ShoppingCart(id=1) + + @hybrid_property + def hybrid_prop_self_referential_list(self) -> List['ShoppingCart']: + return [ShoppingCart(id=1)] diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index d1987326..db9b64d2 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -419,6 +419,9 @@ class Meta: "hybrid_prop_first_shopping_cart_item": ShoppingCartItemType, "hybrid_prop_shopping_cart_item_list": List(ShoppingCartItemType), "hybrid_prop_unsupported_type_tuple": String, + # Self Referential List + "hybrid_prop_self_referential": ShoppingCartType, + "hybrid_prop_self_referential_list": List(ShoppingCartType), } assert sorted(list(ShoppingCartType._meta.fields.keys())) == sorted([ diff --git a/graphene_sqlalchemy/utils.py b/graphene_sqlalchemy/utils.py index f562ff83..301e782c 100644 --- a/graphene_sqlalchemy/utils.py +++ b/graphene_sqlalchemy/utils.py @@ -1,13 +1,15 @@ import re import warnings from collections import OrderedDict -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, Optional import pkg_resources from sqlalchemy.exc import ArgumentError from sqlalchemy.orm import class_mapper, object_mapper from sqlalchemy.orm.exc import UnmappedClassError, UnmappedInstanceError +from graphene_sqlalchemy.registry import get_global_registry + def get_session(context): return context.get("session") @@ -188,3 +190,19 @@ def value_equals(value): """A simple function that makes the equality based matcher functions for SingleDispatchByMatchFunction prettier""" return lambda x: x == value + + +def safe_isinstance(cls): + def safe_isinstance_checker(arg): + try: + return isinstance(arg, cls) + except TypeError: + pass + return safe_isinstance_checker + + +def registry_sqlalchemy_model_from_str(model_name: str) -> Optional[Any]: + try: + return next(filter(lambda x: x.__name__ == model_name, list(get_global_registry()._registry.keys()))) + except StopIteration: + pass From 3acd92be9d9d8a63909f58038c82d36121e62073 Mon Sep 17 00:00:00 2001 From: Cadu Date: Fri, 29 Apr 2022 18:11:34 -0300 Subject: [PATCH 19/19] Extra Tests for Self-referential SQLAlchemy Model Support (in special `A` -> `B` -> `A` being generated via `@hybrid_props`) --- graphene_sqlalchemy/tests/models.py | 4 +++ graphene_sqlalchemy/tests/test_converter.py | 40 ++++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index 0a519882..bda5a863 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -131,6 +131,10 @@ class ShoppingCartItem(Base): id = Column(Integer(), primary_key=True) + @hybrid_property + def hybrid_prop_shopping_cart(self) -> List['ShoppingCart']: + return [ShoppingCart(id=1)] + class ShoppingCart(Base): __tablename__ = "shopping_carts" diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index db9b64d2..4b9e74ed 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -401,7 +401,37 @@ class Meta: model = ShoppingCart interfaces = (Node,) - hybrid_prop_expected_types: Dict[str, Union[Scalar, Structure]] = { + ####################################################### + # Check ShoppingCartItem's Properties and Return Types + ####################################################### + + shopping_cart_item_expected_types: Dict[str, Union[Scalar, Structure]] = { + 'hybrid_prop_shopping_cart': List(ShoppingCartType) + } + + assert sorted(list(ShoppingCartItemType._meta.fields.keys())) == sorted([ + # Columns + "id", + # Append Hybrid Properties from Above + *shopping_cart_item_expected_types.keys() + ]) + + for hybrid_prop_name, hybrid_prop_expected_return_type in shopping_cart_item_expected_types.items(): + hybrid_prop_field = ShoppingCartItemType._meta.fields[hybrid_prop_name] + + # this is a simple way of showing the failed property name + # instead of having to unroll the loop. + assert ( + (hybrid_prop_name, str(hybrid_prop_field.type)) == + (hybrid_prop_name, str(hybrid_prop_expected_return_type)) + ) + assert hybrid_prop_field.description is None # "doc" is ignored by hybrid property + + ################################################### + # Check ShoppingCart's Properties and Return Types + ################################################### + + shopping_cart_expected_types: Dict[str, Union[Scalar, Structure]] = { # Basic types "hybrid_prop_str": String, "hybrid_prop_int": Int, @@ -428,16 +458,16 @@ class Meta: # Columns "id", # Append Hybrid Properties from Above - *hybrid_prop_expected_types.keys() + *shopping_cart_expected_types.keys() ]) - for hybrid_prop_name, hybrid_prop_expected_return_type in hybrid_prop_expected_types.items(): + for hybrid_prop_name, hybrid_prop_expected_return_type in shopping_cart_expected_types.items(): hybrid_prop_field = ShoppingCartType._meta.fields[hybrid_prop_name] # this is a simple way of showing the failed property name # instead of having to unroll the loop. assert ( - (hybrid_prop_name, str(hybrid_prop_field.type)) == - (hybrid_prop_name, str(hybrid_prop_expected_return_type)) + (hybrid_prop_name, str(hybrid_prop_field.type)) == + (hybrid_prop_name, str(hybrid_prop_expected_return_type)) ) assert hybrid_prop_field.description is None # "doc" is ignored by hybrid property