From 1e1751204687f8e95231a4cf6d39783413341fc3 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Thu, 9 Jun 2022 15:42:25 +0200 Subject: [PATCH 01/15] Support for Variant and types.JSON Co-authored-by: Viktor Pegy Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 5 +++++ graphene_sqlalchemy/tests/test_converter.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 60e14ddd..93c2b95d 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -274,10 +274,15 @@ def convert_json_to_string(type, column, registry=None): @convert_sqlalchemy_type.register(JSONType) +@convert_sqlalchemy_type.register(types.JSON) def convert_json_type_to_string(type, column, registry=None): return JSONString +@convert_sqlalchemy_type.register(types.Variant) +def convert_variant_to_impl_type(type, column, registry=None): + return convert_sqlalchemy_type(type.impl, column, registry=registry) + @singledispatchbymatchfunction def convert_sqlalchemy_hybrid_property_type(arg: Any): existing_graphql_type = get_global_registry().get_type_for_model(arg) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 70e11713..0398cbae 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -192,6 +192,16 @@ def test_should_scalar_list_convert_list(): def test_should_jsontype_convert_jsonstring(): assert get_field(JSONType()).type == JSONString + assert get_field(types.JSON).type == JSONString + + +def test_should_variant_int_convert_int(): + assert get_field(types.Variant(types.Integer(), {})).type == graphene.Int + + +def test_should_variant_string_convert_string(): + assert get_field(types.Variant(types.String(), {})).type == graphene.String + def test_should_manytomany_convert_connectionorlist(): From a6e6b8e5539421e28ea7e32dc0baf1f6a9034db4 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Thu, 9 Jun 2022 16:37:01 +0200 Subject: [PATCH 02/15] BREAKING: Date&Time now convert to their corresponding graphene scalars instead of String. Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 15 +++++++++++++-- graphene_sqlalchemy/tests/test_converter.py | 13 ++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 93c2b95d..50ff82f9 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -195,8 +195,6 @@ def convert_sqlalchemy_type(type, column, registry=None): ) -@convert_sqlalchemy_type.register(types.Date) -@convert_sqlalchemy_type.register(types.Time) @convert_sqlalchemy_type.register(types.String) @convert_sqlalchemy_type.register(types.Text) @convert_sqlalchemy_type.register(types.Unicode) @@ -215,6 +213,18 @@ def convert_column_to_datetime(type, column, registry=None): return DateTime +@convert_sqlalchemy_type.register(types.Time) +def convert_column_to_datetime(type, column, registry=None): + from graphene.types.datetime import Time + return Time + + +@convert_sqlalchemy_type.register(types.Date) +def convert_column_to_datetime(type, column, registry=None): + from graphene.types.datetime import Date + return Date + + @convert_sqlalchemy_type.register(types.SmallInteger) @convert_sqlalchemy_type.register(types.Integer) def convert_column_to_int_or_id(type, column, registry=None): @@ -283,6 +293,7 @@ def convert_json_type_to_string(type, column, registry=None): def convert_variant_to_impl_type(type, column, registry=None): return convert_sqlalchemy_type(type.impl, column, registry=registry) + @singledispatchbymatchfunction def convert_sqlalchemy_hybrid_property_type(arg: Any): existing_graphql_type = get_global_registry().get_type_for_model(arg) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 0398cbae..bb074a96 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -58,16 +58,16 @@ def test_should_unknown_sqlalchemy_field_raise_exception(): get_field(getattr(types, 'LargeBinary', types.BINARY)()) -def test_should_date_convert_string(): - assert get_field(types.Date()).type == graphene.String +def test_should_datetime_convert_datetime(): + assert get_field(types.DateTime()).type == graphene.DateTime -def test_should_datetime_convert_datetime(): - assert get_field(types.DateTime()).type == DateTime +def test_should_time_convert_time(): + assert get_field(types.Time()).type == graphene.Time -def test_should_time_convert_string(): - assert get_field(types.Time()).type == graphene.String +def test_should_date_convert_date(): + assert get_field(types.Date()).type == graphene.Date def test_should_string_convert_string(): @@ -203,7 +203,6 @@ def test_should_variant_string_convert_string(): assert get_field(types.Variant(types.String(), {})).type == graphene.String - def test_should_manytomany_convert_connectionorlist(): class A(SQLAlchemyObjectType): class Meta: From 5b861459d6caca5884811294a299e9a20ca30489 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Thu, 9 Jun 2022 16:42:36 +0200 Subject: [PATCH 03/15] Fix naming Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 50ff82f9..18ddd904 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -214,13 +214,13 @@ def convert_column_to_datetime(type, column, registry=None): @convert_sqlalchemy_type.register(types.Time) -def convert_column_to_datetime(type, column, registry=None): +def convert_column_to_time(type, column, registry=None): from graphene.types.datetime import Time return Time @convert_sqlalchemy_type.register(types.Date) -def convert_column_to_datetime(type, column, registry=None): +def convert_column_to_date(type, column, registry=None): from graphene.types.datetime import Date return Date From 2a89227f9127710a5da6b564b54caab299ee0203 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Thu, 9 Jun 2022 17:02:55 +0200 Subject: [PATCH 04/15] BREAKING: PG UUID & sqlalchemy_utils.UUIDType now convert to graphene.UUID instead of graphene.String Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 18 ++++++++++-------- graphene_sqlalchemy/tests/test_converter.py | 8 ++++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 18ddd904..5c880272 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -9,8 +9,8 @@ from sqlalchemy.dialects import postgresql from sqlalchemy.orm import interfaces, strategies -from graphene import (ID, Boolean, Date, DateTime, Dynamic, Enum, Field, Float, - Int, List, String, Time) +from graphene import (ID, Boolean, Date, Time, DateTime, Dynamic, Enum, Field, Float, + Int, List, String, Time, UUID) from graphene.types.json import JSONString from .batching import get_batch_resolver @@ -30,9 +30,9 @@ try: from sqlalchemy_utils import (ChoiceType, JSONType, ScalarListType, - TSVectorType) + TSVectorType, UUIDType) except ImportError: - ChoiceType = JSONType = ScalarListType = TSVectorType = object + ChoiceType = JSONType = ScalarListType = TSVectorType = UUIDType = object try: from sqlalchemy_utils.types.choice import EnumTypeImpl @@ -199,7 +199,6 @@ def convert_sqlalchemy_type(type, column, registry=None): @convert_sqlalchemy_type.register(types.Text) @convert_sqlalchemy_type.register(types.Unicode) @convert_sqlalchemy_type.register(types.UnicodeText) -@convert_sqlalchemy_type.register(postgresql.UUID) @convert_sqlalchemy_type.register(postgresql.INET) @convert_sqlalchemy_type.register(postgresql.CIDR) @convert_sqlalchemy_type.register(TSVectorType) @@ -207,21 +206,24 @@ def convert_column_to_string(type, column, registry=None): return String +@convert_sqlalchemy_type.register(postgresql.UUID) +@convert_sqlalchemy_type.register(UUIDType) +def convert_column_to_uuid(type, column, registry=None): + return UUID + + @convert_sqlalchemy_type.register(types.DateTime) def convert_column_to_datetime(type, column, registry=None): - from graphene.types.datetime import DateTime return DateTime @convert_sqlalchemy_type.register(types.Time) def convert_column_to_time(type, column, registry=None): - from graphene.types.datetime import Time return Time @convert_sqlalchemy_type.register(types.Date) def convert_column_to_date(type, column, registry=None): - from graphene.types.datetime import Date return Date diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index bb074a96..13551285 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -7,7 +7,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.inspection import inspect from sqlalchemy.orm import column_property, composite -from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType +from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, UUIDType import graphene from graphene import Boolean, Float, Int, Scalar, String @@ -300,7 +300,11 @@ class Meta: def test_should_postgresql_uuid_convert(): - assert get_field(postgresql.UUID()).type == graphene.String + assert get_field(postgresql.UUID()).type == graphene.UUID + + +def test_should_sqlalchemy_utils_uuid_convert(): + assert get_field(UUIDType()).type == graphene.UUID def test_should_postgresql_enum_convert(): From dccbe227b1e5a0118061de5840dde81154771e5a Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Thu, 9 Jun 2022 17:31:54 +0200 Subject: [PATCH 05/15] BREAKING: Sort Enums & ChoiceType enums are now generated from Column.key instead of Column.name, see #330 Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 6 +++--- graphene_sqlalchemy/enums.py | 4 ++-- graphene_sqlalchemy/tests/models.py | 9 +++++++-- graphene_sqlalchemy/tests/test_converter.py | 16 ++++++++++++++++ graphene_sqlalchemy/tests/test_sort_enums.py | 20 +++++++++++++++++++- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 5c880272..476de029 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -9,8 +9,8 @@ from sqlalchemy.dialects import postgresql from sqlalchemy.orm import interfaces, strategies -from graphene import (ID, Boolean, Date, Time, DateTime, Dynamic, Enum, Field, Float, - Int, List, String, Time, UUID) +from graphene import (ID, UUID, Boolean, Date, DateTime, Dynamic, Enum, Field, + Float, Int, List, String, Time) from graphene.types.json import JSONString from .batching import get_batch_resolver @@ -253,7 +253,7 @@ def convert_enum_to_enum(type, column, registry=None): # TODO Make ChoiceType conversion consistent with other enums @convert_sqlalchemy_type.register(ChoiceType) def convert_choice_to_enum(type, column, registry=None): - name = "{}_{}".format(column.table.name, column.name).upper() + name = "{}_{}".format(column.table.name, column.key).upper() if isinstance(type.type_impl, EnumTypeImpl): # type.choices may be Enum/IntEnum, in ChoiceType both presented as EnumMeta # do not use from_enum here because we can have more than one enum column in table diff --git a/graphene_sqlalchemy/enums.py b/graphene_sqlalchemy/enums.py index f100be19..a2ed17ad 100644 --- a/graphene_sqlalchemy/enums.py +++ b/graphene_sqlalchemy/enums.py @@ -144,9 +144,9 @@ def sort_enum_for_object_type( column = orm_field.columns[0] if only_indexed and not (column.primary_key or column.index): continue - asc_name = get_name(column.name, True) + asc_name = get_name(column.key, True) asc_value = EnumValue(asc_name, column.asc()) - desc_name = get_name(column.name, False) + desc_name = get_name(column.key, False) desc_value = EnumValue(desc_name, column.desc()) if column.primary_key: default.append(asc_value) diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index e41adb51..7a178210 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -5,8 +5,8 @@ from decimal import Decimal from typing import List, Optional, Tuple -from sqlalchemy import (Column, Date, Enum, ForeignKey, Integer, String, Table, - func, select) +from sqlalchemy import (Column, Date, Enum, ForeignKey, Integer, Numeric, + String, Table, func, select) from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import column_property, composite, mapper, relationship @@ -228,3 +228,8 @@ def hybrid_prop_self_referential_list(self) -> List['ShoppingCart']: @hybrid_property def hybrid_prop_optional_self_referential(self) -> Optional['ShoppingCart']: return None + +class KeyedModel(Base): + __tablename__ = "test330" + id = Column(Integer(), primary_key=True) + reporter_number = Column("% reporter_number", Numeric, key="reporter_number") diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 13551285..225294e8 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -163,6 +163,22 @@ class TestEnum(enum.Enum): assert graphene_type._meta.enum.__members__["en"].value == "English" +def test_choice_enum_column_key_name_issue_301(): + 'Support column.key instead of column.name for enum naming' + class TestEnum(enum.Enum): + es = u"Spanish" + en = u"English" + + testChoice = Column("% descuento1", ChoiceType(TestEnum, impl=types.String()), key="descuento1") + field = get_field_from_column(testChoice) + + graphene_type = field.type + assert issubclass(graphene_type, graphene.Enum) + assert graphene_type._meta.name == "MODEL_DESCUENTO1" + assert graphene_type._meta.enum.__members__["es"].value == "Spanish" + assert graphene_type._meta.enum.__members__["en"].value == "English" + + def test_should_intenum_choice_convert_enum(): class TestEnum(enum.IntEnum): one = 1 diff --git a/graphene_sqlalchemy/tests/test_sort_enums.py b/graphene_sqlalchemy/tests/test_sort_enums.py index 6291d4f8..56286837 100644 --- a/graphene_sqlalchemy/tests/test_sort_enums.py +++ b/graphene_sqlalchemy/tests/test_sort_enums.py @@ -7,7 +7,7 @@ from ..fields import SQLAlchemyConnectionField from ..types import SQLAlchemyObjectType from ..utils import to_type_name -from .models import Base, HairKind, Pet +from .models import Base, HairKind, KeyedModel, Pet from .test_query import to_std_dicts @@ -383,3 +383,21 @@ def makeNodes(nodeList): assert [node["node"]["name"] for node in result.data["noSort"]["edges"]] == [ node["node"]["name"] for node in result.data["noDefaultSort"]["edges"] ] + + +def test_sort_enum_from_key_issue_330(): + class KeyedType(SQLAlchemyObjectType): + class Meta: + model = KeyedModel + + sort_enum = KeyedType.sort_enum() + assert isinstance(sort_enum, type(Enum)) + assert sort_enum._meta.name == "KeyedTypeSortEnum" + assert list(sort_enum._meta.enum.__members__) == [ + "ID_ASC", + "ID_DESC", + "REPORTER_NUMBER_ASC", + "REPORTER_NUMBER_DESC", + ] + assert str(sort_enum.REPORTER_NUMBER_ASC.value.value) == 'test330."% reporter_number" ASC' + assert str(sort_enum.REPORTER_NUMBER_DESC.value.value) == 'test330."% reporter_number" DESC' From 18b6bf1977db57969c065204d4f6209ca0523884 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Thu, 9 Jun 2022 17:41:36 +0200 Subject: [PATCH 06/15] Mention Co-Authors for converters&enum Co-authored-by: Nicolas Delaby Co-authored-by: davidcim Signed-off-by: Erik Wrede --- graphene_sqlalchemy/tests/test_converter.py | 5 ++++- graphene_sqlalchemy/tests/test_sort_enums.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 225294e8..aeeebfd9 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -164,7 +164,10 @@ class TestEnum(enum.Enum): def test_choice_enum_column_key_name_issue_301(): - 'Support column.key instead of column.name for enum naming' + """ + Verifies that the sort enum name is generated from the column key instead of the name, + in case the column has an invalid enum name. See #330 + """ class TestEnum(enum.Enum): es = u"Spanish" en = u"English" diff --git a/graphene_sqlalchemy/tests/test_sort_enums.py b/graphene_sqlalchemy/tests/test_sort_enums.py index 56286837..e2510abc 100644 --- a/graphene_sqlalchemy/tests/test_sort_enums.py +++ b/graphene_sqlalchemy/tests/test_sort_enums.py @@ -386,6 +386,11 @@ def makeNodes(nodeList): def test_sort_enum_from_key_issue_330(): + """ + Verifies that the sort enum name is generated from the column key instead of the name, + in case the column has an invalid enum name. See #330 + """ + class KeyedType(SQLAlchemyObjectType): class Meta: model = KeyedModel From 1ccd8c103776dd8d54e7e993bf0b4a1db30e3e4b Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Fri, 10 Jun 2022 12:14:57 +0200 Subject: [PATCH 07/15] Draft: Add Union type conversion support --- graphene_sqlalchemy/converter.py | 62 +++++++++++++++++++++++++++----- graphene_sqlalchemy/registry.py | 36 ++++++++++++++----- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 476de029..566dc8be 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,14 +1,16 @@ import datetime +import sys import typing import warnings from decimal import Decimal from functools import singledispatch -from typing import Any +from typing import Any, cast from sqlalchemy import types from sqlalchemy.dialects import postgresql from sqlalchemy.orm import interfaces, strategies +import graphene from graphene import (ID, UUID, Boolean, Date, DateTime, Dynamic, Enum, Field, Float, Int, List, String, Time) from graphene.types.json import JSONString @@ -17,7 +19,7 @@ from .enums import enum_for_sa_enum from .fields import (BatchSQLAlchemyConnectionField, default_connection_field_factory) -from .registry import get_global_registry +from .registry import Registry, get_global_registry from .resolvers import get_attr_resolver, get_custom_resolver from .utils import (registry_sqlalchemy_model_from_str, safe_isinstance, singledispatchbymatchfunction, value_equals) @@ -353,16 +355,60 @@ def convert_sqlalchemy_hybrid_property_type_time(arg): return Time -@convert_sqlalchemy_hybrid_property_type.register(lambda x: getattr(x, '__origin__', None) == typing.Union) -def convert_sqlalchemy_hybrid_property_type_option_t(arg): - # Option is actually Union[T, ] +def is_union(arg) -> bool: + if sys.version_info >= (3, 10): + from types import UnionType + + if isinstance(arg, UnionType): + return True + return getattr(arg, '__origin__', None) == typing.Union + + +def graphene_union_for_py_enum(types: tuple[graphene.ObjectType], registry: Registry): + union_type = registry.get_union_for_object_types(tuple(types)) + + if union_type is None: + pass + # pass + # TODO + + +@convert_sqlalchemy_hybrid_property_type.register(is_union) +def convert_sqlalchemy_hybrid_property_union(arg): + """ + Converts Unions (Union[X,Y], or X | Y for python > 3.10) to the corresponding graphene schema object. + Since Optionals are internally represented as Union[T, ], they are handled here as well. + The GQL Spec currently only allows for ObjectType unions: + GraphQL Unions represent an object that could be one of a list of GraphQL Object types, but provides for no + guaranteed fields between those types. + That's why we have to check for the nested types to be instances of graphene.ObjectType, except for the union case. + + type(x) == _types.UnionType is necessary to support X | Y notation, but might break in future python releases. + """ + # Option is actually Union[T, ] + isinstance() # Just get the T out of the list of arguments by filtering out the NoneType - internal_type = next(filter(lambda x: not type(None) == x, arg.__args__)) + nested_types = list(filter(lambda x: not type(None) == x, arg.__args__)) - graphql_internal_type = convert_sqlalchemy_hybrid_property_type(internal_type) + # If only one type is left after filtering out NoneType, the Union was an Optional + if len(nested_types) == 1: + graphql_internal_type = convert_sqlalchemy_hybrid_property_type(nested_types[0]) + return graphql_internal_type + + # Map the graphene types to the nested types. + # We use convert_sqlalchemy_hybrid_property_type instead of the registry to account for ForwardRefs, Lists,... + graphene_types = map(convert_sqlalchemy_hybrid_property_type, nested_types) + + # Now check, if every type is instance of an ObjectType + object_type_check = [isinstance(graphene_type, graphene.ObjectType) for graphene_type in graphene_types] + + if object_type_check.count(False) != 0: + raise ValueError("Cannot convert hybrid_property Union to graphene.Union: the Union contains scalars. " + "Please add the corresponding hybrid_property to the excluded fields in the ObjectType, " + "or use an ORMField to override this behaviour.") - return graphql_internal_type + return graphene_union_for_py_enum(cast(tuple[graphene.ObjectType], tuple(graphene_types)), get_global_registry()) @convert_sqlalchemy_hybrid_property_type.register(lambda x: getattr(x, '__origin__', None) in [list, typing.List]) diff --git a/graphene_sqlalchemy/registry.py b/graphene_sqlalchemy/registry.py index acfa744b..166361ed 100644 --- a/graphene_sqlalchemy/registry.py +++ b/graphene_sqlalchemy/registry.py @@ -2,8 +2,11 @@ from sqlalchemy.types import Enum as SQLAlchemyEnumType +import graphene from graphene import Enum +from .types import SQLAlchemyObjectType + class Registry(object): def __init__(self): @@ -13,12 +16,13 @@ def __init__(self): self._registry_composites = {} self._registry_enums = {} self._registry_sort_enums = {} + self._registry_unions = {} def register(self, obj_type): from .types import SQLAlchemyObjectType if not isinstance(obj_type, type) or not issubclass( - obj_type, SQLAlchemyObjectType + obj_type, SQLAlchemyObjectType ): raise TypeError( "Expected SQLAlchemyObjectType, but got: {!r}".format(obj_type) @@ -37,7 +41,7 @@ def register_orm_field(self, obj_type, field_name, orm_field): from .types import SQLAlchemyObjectType if not isinstance(obj_type, type) or not issubclass( - obj_type, SQLAlchemyObjectType + obj_type, SQLAlchemyObjectType ): raise TypeError( "Expected SQLAlchemyObjectType, but got: {!r}".format(obj_type) @@ -55,7 +59,7 @@ def register_composite_converter(self, composite, converter): def get_converter_for_composite(self, composite): return self._registry_composites.get(composite) - def register_enum(self, sa_enum, graphene_enum): + def register_enum(self, sa_enum: SQLAlchemyEnumType, graphene_enum: Enum): if not isinstance(sa_enum, SQLAlchemyEnumType): raise TypeError( "Expected SQLAlchemyEnumType, but got: {!r}".format(sa_enum) @@ -67,14 +71,13 @@ def register_enum(self, sa_enum, graphene_enum): self._registry_enums[sa_enum] = graphene_enum - def get_graphene_enum_for_sa_enum(self, sa_enum): + def get_graphene_enum_for_sa_enum(self, sa_enum : SQLAlchemyEnumType): return self._registry_enums.get(sa_enum) - def register_sort_enum(self, obj_type, sort_enum): - from .types import SQLAlchemyObjectType + def register_sort_enum(self, obj_type: SQLAlchemyObjectType, sort_enum: Enum): if not isinstance(obj_type, type) or not issubclass( - obj_type, SQLAlchemyObjectType + obj_type, SQLAlchemyObjectType ): raise TypeError( "Expected SQLAlchemyObjectType, but got: {!r}".format(obj_type) @@ -83,9 +86,26 @@ def register_sort_enum(self, obj_type, sort_enum): raise TypeError("Expected Graphene Enum, but got: {!r}".format(sort_enum)) self._registry_sort_enums[obj_type] = sort_enum - def get_sort_enum_for_object_type(self, obj_type): + def get_sort_enum_for_object_type(self, obj_type: graphene.ObjectType): return self._registry_sort_enums.get(obj_type) + def register_union_type(self, union: graphene.Union, types: tuple[graphene.ObjectType]): + if not isinstance(union, graphene.Union): + raise TypeError( + "Expected graphene.Union, but got: {!r}".format(union) + ) + + for object_type in types: + if not isinstance(object_type, graphene.ObjectType): + raise TypeError( + "Expected Graphene ObjectType, but got: {!r}".format(object_type) + ) + + self._registry_enums[tuple(types)] = union + + def get_union_for_object_types(self, types: tuple[graphene.ObjectType]): + self._registry_unions.get(tuple(types)) + registry = None From 6e580273c3ac56c5d40f7e8d436272370b7dad76 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Fri, 10 Jun 2022 16:36:07 +0200 Subject: [PATCH 08/15] Fixed Pytest Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 37 +++++++++++++------------ graphene_sqlalchemy/registry.py | 24 ++++++++-------- graphene_sqlalchemy/tests/models.py | 1 + graphene_sqlalchemy/tests/test_types.py | 2 +- graphene_sqlalchemy/utils.py | 3 +- 5 files changed, 35 insertions(+), 32 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 566dc8be..fbc6cf28 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -19,7 +19,7 @@ from .enums import enum_for_sa_enum from .fields import (BatchSQLAlchemyConnectionField, default_connection_field_factory) -from .registry import Registry, get_global_registry + from .resolvers import get_attr_resolver, get_custom_resolver from .utils import (registry_sqlalchemy_model_from_str, safe_isinstance, singledispatchbymatchfunction, value_equals) @@ -249,6 +249,7 @@ def convert_column_to_float(type, column, registry=None): @convert_sqlalchemy_type.register(types.Enum) def convert_enum_to_enum(type, column, registry=None): + from .registry import get_global_registry return lambda: enum_for_sa_enum(type, registry or get_global_registry()) @@ -300,6 +301,7 @@ def convert_variant_to_impl_type(type, column, registry=None): @singledispatchbymatchfunction def convert_sqlalchemy_hybrid_property_type(arg: Any): + from .registry import get_global_registry existing_graphql_type = get_global_registry().get_type_for_model(arg) if existing_graphql_type: return existing_graphql_type @@ -364,13 +366,16 @@ def is_union(arg) -> bool: return getattr(arg, '__origin__', None) == typing.Union -def graphene_union_for_py_enum(types: tuple[graphene.ObjectType], registry: Registry): - union_type = registry.get_union_for_object_types(tuple(types)) +def graphene_union_for_py_enum(obj_types: list[graphene.ObjectType], registry) -> graphene.Union: + union_type = registry.get_union_for_object_types(obj_types) if union_type is None: - pass - # pass - # TODO + # Union Name is name of the three + union_name = ''.join(sorted([obj_type._meta.name for obj_type in obj_types])) + union_type = graphene.Union(union_name, obj_types) + registry.register_union_type(union_type, obj_types) + + return union_type @convert_sqlalchemy_hybrid_property_type.register(is_union) @@ -386,29 +391,26 @@ def convert_sqlalchemy_hybrid_property_union(arg): type(x) == _types.UnionType is necessary to support X | Y notation, but might break in future python releases. """ + from .registry import get_global_registry # Option is actually Union[T, ] - isinstance() # Just get the T out of the list of arguments by filtering out the NoneType nested_types = list(filter(lambda x: not type(None) == x, arg.__args__)) - # If only one type is left after filtering out NoneType, the Union was an Optional - if len(nested_types) == 1: - graphql_internal_type = convert_sqlalchemy_hybrid_property_type(nested_types[0]) - return graphql_internal_type - # Map the graphene types to the nested types. # We use convert_sqlalchemy_hybrid_property_type instead of the registry to account for ForwardRefs, Lists,... - graphene_types = map(convert_sqlalchemy_hybrid_property_type, nested_types) + graphene_types = list(map(convert_sqlalchemy_hybrid_property_type, nested_types)) - # Now check, if every type is instance of an ObjectType - object_type_check = [isinstance(graphene_type, graphene.ObjectType) for graphene_type in graphene_types] + # If only one type is left after filtering out NoneType, the Union was an Optional + if len(graphene_types) == 1: + return graphene_types[0] - if object_type_check.count(False) != 0: + # Now check if every type is instance of an ObjectType + if not all(isinstance(graphene_type, graphene.ObjectType) for graphene_type in graphene_types): raise ValueError("Cannot convert hybrid_property Union to graphene.Union: the Union contains scalars. " "Please add the corresponding hybrid_property to the excluded fields in the ObjectType, " "or use an ORMField to override this behaviour.") - return graphene_union_for_py_enum(cast(tuple[graphene.ObjectType], tuple(graphene_types)), get_global_registry()) + return graphene_union_for_py_enum(cast(list[graphene.ObjectType], list(graphene_types)), get_global_registry()) @convert_sqlalchemy_hybrid_property_type.register(lambda x: getattr(x, '__origin__', None) in [list, typing.List]) @@ -427,6 +429,7 @@ def convert_sqlalchemy_hybrid_property_forwardref(arg): Generate a lambda that will resolve the type at runtime This takes care of self-references """ + from .registry import get_global_registry def forward_reference_solver(): model = registry_sqlalchemy_model_from_str(arg.__forward_arg__) diff --git a/graphene_sqlalchemy/registry.py b/graphene_sqlalchemy/registry.py index 166361ed..329b0f96 100644 --- a/graphene_sqlalchemy/registry.py +++ b/graphene_sqlalchemy/registry.py @@ -1,12 +1,11 @@ from collections import defaultdict +from typing import List from sqlalchemy.types import Enum as SQLAlchemyEnumType import graphene from graphene import Enum -from .types import SQLAlchemyObjectType - class Registry(object): def __init__(self): @@ -19,8 +18,8 @@ def __init__(self): self._registry_unions = {} def register(self, obj_type): - from .types import SQLAlchemyObjectType + from .types import SQLAlchemyObjectType if not isinstance(obj_type, type) or not issubclass( obj_type, SQLAlchemyObjectType ): @@ -71,11 +70,12 @@ def register_enum(self, sa_enum: SQLAlchemyEnumType, graphene_enum: Enum): self._registry_enums[sa_enum] = graphene_enum - def get_graphene_enum_for_sa_enum(self, sa_enum : SQLAlchemyEnumType): + def get_graphene_enum_for_sa_enum(self, sa_enum: SQLAlchemyEnumType): return self._registry_enums.get(sa_enum) - def register_sort_enum(self, obj_type: SQLAlchemyObjectType, sort_enum: Enum): + def register_sort_enum(self, obj_type, sort_enum: Enum): + from .types import SQLAlchemyObjectType if not isinstance(obj_type, type) or not issubclass( obj_type, SQLAlchemyObjectType ): @@ -89,22 +89,22 @@ def register_sort_enum(self, obj_type: SQLAlchemyObjectType, sort_enum: Enum): def get_sort_enum_for_object_type(self, obj_type: graphene.ObjectType): return self._registry_sort_enums.get(obj_type) - def register_union_type(self, union: graphene.Union, types: tuple[graphene.ObjectType]): + def register_union_type(self, union: graphene.Union, obj_types: list[graphene.ObjectType]): if not isinstance(union, graphene.Union): raise TypeError( "Expected graphene.Union, but got: {!r}".format(union) ) - for object_type in types: - if not isinstance(object_type, graphene.ObjectType): + for obj_type in obj_types: + if not isinstance(obj_type, graphene.ObjectType): raise TypeError( - "Expected Graphene ObjectType, but got: {!r}".format(object_type) + "Expected Graphene ObjectType, but got: {!r}".format(obj_type) ) - self._registry_enums[tuple(types)] = union + self._registry_enums[tuple(sorted(obj_types))] = union - def get_union_for_object_types(self, types: tuple[graphene.ObjectType]): - self._registry_unions.get(tuple(types)) + def get_union_for_object_types(self, obj_types: List[graphene.ObjectType]): + self._registry_unions.get(tuple(sorted(obj_types))) registry = None diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index 7a178210..dc399ee0 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -229,6 +229,7 @@ def hybrid_prop_self_referential_list(self) -> List['ShoppingCart']: def hybrid_prop_optional_self_referential(self) -> Optional['ShoppingCart']: return None + class KeyedModel(Base): __tablename__ = "test330" id = Column(Integer(), primary_key=True) diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index 9a2e992d..00e8b3af 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -76,7 +76,7 @@ class Meta: assert sorted(list(ReporterType._meta.fields.keys())) == sorted([ # Columns - "column_prop", # SQLAlchemy retuns column properties first + "column_prop", "id", "first_name", "last_name", diff --git a/graphene_sqlalchemy/utils.py b/graphene_sqlalchemy/utils.py index 084f9b86..e02b1439 100644 --- a/graphene_sqlalchemy/utils.py +++ b/graphene_sqlalchemy/utils.py @@ -8,8 +8,6 @@ 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") @@ -203,6 +201,7 @@ def safe_isinstance_checker(arg): def registry_sqlalchemy_model_from_str(model_name: str) -> Optional[Any]: + from graphene_sqlalchemy.registry import get_global_registry try: return next(filter(lambda x: x.__name__ == model_name, list(get_global_registry()._registry.keys()))) except StopIteration: From 4c27ccf1ebc9142ef686e42cc835e75eca9668ae Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Thu, 23 Jun 2022 11:55:39 +0200 Subject: [PATCH 09/15] Fix Tests & reformatting --- graphene_sqlalchemy/converter.py | 98 ++++++++++----------- graphene_sqlalchemy/tests/test_converter.py | 62 +++++++++++-- 2 files changed, 102 insertions(+), 58 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index fbc6cf28..2b539f04 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -6,20 +6,17 @@ from functools import singledispatch from typing import Any, cast -from sqlalchemy import types +from sqlalchemy import types as sqa_types from sqlalchemy.dialects import postgresql from sqlalchemy.orm import interfaces, strategies import graphene -from graphene import (ID, UUID, Boolean, Date, DateTime, Dynamic, Enum, Field, - Float, Int, List, String, Time) from graphene.types.json import JSONString from .batching import get_batch_resolver from .enums import enum_for_sa_enum from .fields import (BatchSQLAlchemyConnectionField, default_connection_field_factory) - from .resolvers import get_attr_resolver, get_custom_resolver from .utils import (registry_sqlalchemy_model_from_str, safe_isinstance, singledispatchbymatchfunction, value_equals) @@ -81,7 +78,7 @@ def dynamic_type(): return _convert_o2m_or_m2m_relationship(relationship_prop, obj_type, batching_, connection_field_factory, **field_kwargs) - return Dynamic(dynamic_type) + return graphene.Dynamic(dynamic_type) def _convert_o2o_or_m2o_relationship(relationship_prop, obj_type, batching, orm_field_name, **field_kwargs): @@ -102,7 +99,7 @@ def _convert_o2o_or_m2o_relationship(relationship_prop, obj_type, batching, orm_ resolver = get_batch_resolver(relationship_prop) if batching else \ get_attr_resolver(obj_type, relationship_prop.key) - return Field(child_type, resolver=resolver, **field_kwargs) + return graphene.Field(child_type, resolver=resolver, **field_kwargs) def _convert_o2m_or_m2m_relationship(relationship_prop, obj_type, batching, connection_field_factory, **field_kwargs): @@ -119,7 +116,7 @@ def _convert_o2m_or_m2m_relationship(relationship_prop, obj_type, batching, conn child_type = obj_type._meta.registry.get_type_for_model(relationship_prop.mapper.entity) if not child_type._meta.connection: - return Field(List(child_type), **field_kwargs) + return graphene.Field(graphene.List(child_type), **field_kwargs) # TODO Allow override of connection_field_factory and resolver via ORMField if connection_field_factory is None: @@ -136,7 +133,7 @@ def convert_sqlalchemy_hybrid_method(hybrid_prop, resolver, **field_kwargs): if 'description' not in field_kwargs: field_kwargs['description'] = getattr(hybrid_prop, "__doc__", None) - return Field( + return graphene.Field( resolver=resolver, **field_kwargs ) @@ -183,7 +180,7 @@ def convert_sqlalchemy_column(column_prop, registry, resolver, **field_kwargs): field_kwargs.setdefault('required', not is_column_nullable(column)) field_kwargs.setdefault('description', get_column_doc(column)) - return Field( + return graphene.Field( resolver=resolver, **field_kwargs ) @@ -197,57 +194,57 @@ def convert_sqlalchemy_type(type, column, registry=None): ) -@convert_sqlalchemy_type.register(types.String) -@convert_sqlalchemy_type.register(types.Text) -@convert_sqlalchemy_type.register(types.Unicode) -@convert_sqlalchemy_type.register(types.UnicodeText) +@convert_sqlalchemy_type.register(sqa_types.String) +@convert_sqlalchemy_type.register(sqa_types.Text) +@convert_sqlalchemy_type.register(sqa_types.Unicode) +@convert_sqlalchemy_type.register(sqa_types.UnicodeText) @convert_sqlalchemy_type.register(postgresql.INET) @convert_sqlalchemy_type.register(postgresql.CIDR) @convert_sqlalchemy_type.register(TSVectorType) def convert_column_to_string(type, column, registry=None): - return String + return graphene.String @convert_sqlalchemy_type.register(postgresql.UUID) @convert_sqlalchemy_type.register(UUIDType) def convert_column_to_uuid(type, column, registry=None): - return UUID + return graphene.UUID -@convert_sqlalchemy_type.register(types.DateTime) +@convert_sqlalchemy_type.register(sqa_types.DateTime) def convert_column_to_datetime(type, column, registry=None): - return DateTime + return graphene.DateTime -@convert_sqlalchemy_type.register(types.Time) +@convert_sqlalchemy_type.register(sqa_types.Time) def convert_column_to_time(type, column, registry=None): - return Time + return graphene.Time -@convert_sqlalchemy_type.register(types.Date) +@convert_sqlalchemy_type.register(sqa_types.Date) def convert_column_to_date(type, column, registry=None): - return Date + return graphene.Date -@convert_sqlalchemy_type.register(types.SmallInteger) -@convert_sqlalchemy_type.register(types.Integer) +@convert_sqlalchemy_type.register(sqa_types.SmallInteger) +@convert_sqlalchemy_type.register(sqa_types.Integer) def convert_column_to_int_or_id(type, column, registry=None): - return ID if column.primary_key else Int + return graphene.ID if column.primary_key else graphene.Int -@convert_sqlalchemy_type.register(types.Boolean) +@convert_sqlalchemy_type.register(sqa_types.Boolean) def convert_column_to_boolean(type, column, registry=None): - return Boolean + return graphene.Boolean -@convert_sqlalchemy_type.register(types.Float) -@convert_sqlalchemy_type.register(types.Numeric) -@convert_sqlalchemy_type.register(types.BigInteger) +@convert_sqlalchemy_type.register(sqa_types.Float) +@convert_sqlalchemy_type.register(sqa_types.Numeric) +@convert_sqlalchemy_type.register(sqa_types.BigInteger) def convert_column_to_float(type, column, registry=None): - return Float + return graphene.Float -@convert_sqlalchemy_type.register(types.Enum) +@convert_sqlalchemy_type.register(sqa_types.Enum) def convert_enum_to_enum(type, column, registry=None): from .registry import get_global_registry return lambda: enum_for_sa_enum(type, registry or get_global_registry()) @@ -260,25 +257,25 @@ def convert_choice_to_enum(type, column, registry=None): if isinstance(type.type_impl, EnumTypeImpl): # type.choices may be Enum/IntEnum, in ChoiceType both presented as EnumMeta # do not use from_enum here because we can have more than one enum column in table - return Enum(name, list((v.name, v.value) for v in type.choices)) + return graphene.Enum(name, list((v.name, v.value) for v in type.choices)) else: - return Enum(name, type.choices) + return graphene.Enum(name, type.choices) @convert_sqlalchemy_type.register(ScalarListType) def convert_scalar_list_to_list(type, column, registry=None): - return List(String) + return graphene.List(graphene.String) 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 graphene.List(init_array_list_recursive(inner_type, n - 1)) -@convert_sqlalchemy_type.register(types.ARRAY) +@convert_sqlalchemy_type.register(sqa_types.ARRAY) @convert_sqlalchemy_type.register(postgresql.ARRAY) def convert_array_to_list(_type, column, registry=None): inner_type = convert_sqlalchemy_type(column.type.item_type, column) - return List(init_array_list_recursive(inner_type, (column.type.dimensions or 1) - 1)) + return graphene.List(init_array_list_recursive(inner_type, (column.type.dimensions or 1) - 1)) @convert_sqlalchemy_type.register(postgresql.HSTORE) @@ -289,12 +286,12 @@ def convert_json_to_string(type, column, registry=None): @convert_sqlalchemy_type.register(JSONType) -@convert_sqlalchemy_type.register(types.JSON) +@convert_sqlalchemy_type.register(sqa_types.JSON) def convert_json_type_to_string(type, column, registry=None): return JSONString -@convert_sqlalchemy_type.register(types.Variant) +@convert_sqlalchemy_type.register(sqa_types.Variant) def convert_variant_to_impl_type(type, column, registry=None): return convert_sqlalchemy_type(type.impl, column, registry=registry) @@ -311,22 +308,22 @@ def convert_sqlalchemy_hybrid_property_type(arg: Any): (f"I don't know how to generate a GraphQL type out of a \"{arg}\" type." "Falling back to \"graphene.String\"") ) - return String + return graphene.String @convert_sqlalchemy_hybrid_property_type.register(value_equals(str)) def convert_sqlalchemy_hybrid_property_type_str(arg): - return String + return graphene.String @convert_sqlalchemy_hybrid_property_type.register(value_equals(int)) def convert_sqlalchemy_hybrid_property_type_int(arg): - return Int + return graphene.Int @convert_sqlalchemy_hybrid_property_type.register(value_equals(float)) def convert_sqlalchemy_hybrid_property_type_float(arg): - return Float + return graphene.Float @convert_sqlalchemy_hybrid_property_type.register(value_equals(Decimal)) @@ -334,27 +331,27 @@ 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 + return graphene.String @convert_sqlalchemy_hybrid_property_type.register(value_equals(bool)) def convert_sqlalchemy_hybrid_property_type_bool(arg): - return Boolean + return graphene.Boolean @convert_sqlalchemy_hybrid_property_type.register(value_equals(datetime.datetime)) def convert_sqlalchemy_hybrid_property_type_datetime(arg): - return DateTime + return graphene.DateTime @convert_sqlalchemy_hybrid_property_type.register(value_equals(datetime.date)) def convert_sqlalchemy_hybrid_property_type_date(arg): - return Date + return graphene.Date @convert_sqlalchemy_hybrid_property_type.register(value_equals(datetime.time)) def convert_sqlalchemy_hybrid_property_type_time(arg): - return Time + return graphene.Time def is_union(arg) -> bool: @@ -392,6 +389,7 @@ def convert_sqlalchemy_hybrid_property_union(arg): type(x) == _types.UnionType is necessary to support X | Y notation, but might break in future python releases. """ from .registry import get_global_registry + # Option is actually Union[T, ] # Just get the T out of the list of arguments by filtering out the NoneType nested_types = list(filter(lambda x: not type(None) == x, arg.__args__)) @@ -420,7 +418,7 @@ def convert_sqlalchemy_hybrid_property_type_list_t(arg): graphql_internal_type = convert_sqlalchemy_hybrid_property_type(internal_type) - return List(graphql_internal_type) + return graphene.List(graphql_internal_type) @convert_sqlalchemy_hybrid_property_type.register(safe_isinstance(ForwardRef)) @@ -434,7 +432,7 @@ def convert_sqlalchemy_hybrid_property_forwardref(arg): def forward_reference_solver(): model = registry_sqlalchemy_model_from_str(arg.__forward_arg__) if not model: - return String + return graphene.String # Always fall back to string if no ForwardRef type found. return get_global_registry().get_type_for_model(model) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index aeeebfd9..47527c12 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -1,10 +1,12 @@ import enum +import sys from typing import Dict, Union import pytest from sqlalchemy import Column, func, select, types from sqlalchemy.dialects import postgresql from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.inspection import inspect from sqlalchemy.orm import column_property, composite from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, UUIDType @@ -18,11 +20,12 @@ from ..converter import (convert_sqlalchemy_column, convert_sqlalchemy_composite, + convert_sqlalchemy_hybrid_method, convert_sqlalchemy_relationship) from ..fields import (UnsortedSQLAlchemyConnectionField, default_connection_field_factory) from ..registry import Registry, get_global_registry -from ..types import SQLAlchemyObjectType +from ..types import ORMField, SQLAlchemyObjectType from .models import (Article, CompositeFullName, Pet, Reporter, ShoppingCart, ShoppingCartItem) @@ -51,7 +54,49 @@ class Model(declarative_base()): return convert_sqlalchemy_column(column_prop, get_global_registry(), mock_resolver) -def test_should_unknown_sqlalchemy_field_raise_exception(): +def get_hybrid_property_type(prop_method): + class Model(declarative_base()): + __tablename__ = 'model' + id_ = Column(types.Integer, primary_key=True) + prop = prop_method + + column_prop = inspect(Model).all_orm_descriptors['prop'] + return convert_sqlalchemy_hybrid_method(column_prop, mock_resolver(), **ORMField().kwargs) + + +def test_hybrid_prop_int(): + @hybrid_property + def prop_method() -> int: + return 42 + + assert get_hybrid_property_type(prop_method).type == graphene.Int + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="|-Style Unions are unsupported in python < 3.10") +def test_hybrid_prop_scalar_union_310(): + @hybrid_property + def prop_method() -> int | str: + return "not allowed in gql schema" + + with pytest.raises(ValueError, + match=r"Cannot convert hybrid_property Union to " + r"graphene.Union: the Union contains scalars. \.*"): + get_hybrid_property_type(prop_method) + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="|-Style Unions are unsupported in python < 3.10") +def test_hybrid_prop_scalar_union_and_optional_310(): + """Checks if the use of Optionals does not interfere with non-conform scalar return types""" + + @hybrid_property + def prop_method() -> int | None: + return 42 + + assert get_hybrid_property_type(prop_method).type == graphene.Int + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="|-Style Unions are unsupported in python < 3.10") +def test_should_unknown_sqlalchemy_field_raise_exception_310(): re_err = "Don't know how to convert the SQLAlchemy field" with pytest.raises(Exception, match=re_err): # support legacy Binary type and subsequent LargeBinary @@ -168,6 +213,7 @@ def test_choice_enum_column_key_name_issue_301(): Verifies that the sort enum name is generated from the column key instead of the name, in case the column has an invalid enum name. See #330 """ + class TestEnum(enum.Enum): es = u"Spanish" en = u"English" @@ -453,9 +499,9 @@ class Meta: # 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_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 @@ -500,8 +546,8 @@ class Meta: # 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_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 b1dd214c974022735eb279d4ff1cd05ea8b2b4ba Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Thu, 23 Jun 2022 19:08:43 +0200 Subject: [PATCH 10/15] Fixed 3.7 style type annotations Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 4 ++-- graphene_sqlalchemy/registry.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 2b539f04..1b30c3d8 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -363,7 +363,7 @@ def is_union(arg) -> bool: return getattr(arg, '__origin__', None) == typing.Union -def graphene_union_for_py_enum(obj_types: list[graphene.ObjectType], registry) -> graphene.Union: +def graphene_union_for_py_union(obj_types: typing.List[graphene.ObjectType], registry) -> graphene.Union: union_type = registry.get_union_for_object_types(obj_types) if union_type is None: @@ -408,7 +408,7 @@ def convert_sqlalchemy_hybrid_property_union(arg): "Please add the corresponding hybrid_property to the excluded fields in the ObjectType, " "or use an ORMField to override this behaviour.") - return graphene_union_for_py_enum(cast(list[graphene.ObjectType], list(graphene_types)), get_global_registry()) + return graphene_union_for_py_union(cast(typing.List[graphene.ObjectType], list(graphene_types)), get_global_registry()) @convert_sqlalchemy_hybrid_property_type.register(lambda x: getattr(x, '__origin__', None) in [list, typing.List]) diff --git a/graphene_sqlalchemy/registry.py b/graphene_sqlalchemy/registry.py index 329b0f96..bf0f31a1 100644 --- a/graphene_sqlalchemy/registry.py +++ b/graphene_sqlalchemy/registry.py @@ -89,7 +89,7 @@ def register_sort_enum(self, obj_type, sort_enum: Enum): def get_sort_enum_for_object_type(self, obj_type: graphene.ObjectType): return self._registry_sort_enums.get(obj_type) - def register_union_type(self, union: graphene.Union, obj_types: list[graphene.ObjectType]): + def register_union_type(self, union: graphene.Union, obj_types: List[graphene.ObjectType]): if not isinstance(union, graphene.Union): raise TypeError( "Expected graphene.Union, but got: {!r}".format(union) From 95db9f96d0c8383c7b537f56877b9e5c58f035c8 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Thu, 23 Jun 2022 19:56:03 +0200 Subject: [PATCH 11/15] Remove static imports to graphene to increase readability due to ambivalent type names (in most cases) Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 22 ++++---- graphene_sqlalchemy/tests/test_converter.py | 62 ++++++++++----------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 1b30c3d8..f260861e 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -19,7 +19,7 @@ default_connection_field_factory) from .resolvers import get_attr_resolver, get_custom_resolver from .utils import (registry_sqlalchemy_model_from_str, safe_isinstance, - singledispatchbymatchfunction, value_equals) + singledispatchbymatchfunction, value_equals, DummyImport) try: from typing import ForwardRef @@ -28,13 +28,12 @@ from typing import _ForwardRef as ForwardRef try: - from sqlalchemy_utils import (ChoiceType, JSONType, ScalarListType, - TSVectorType, UUIDType) + import sqlalchemy_utils as sqa_utils except ImportError: - ChoiceType = JSONType = ScalarListType = TSVectorType = UUIDType = object + sqlalchemy_utils = DummyImport() try: - from sqlalchemy_utils.types.choice import EnumTypeImpl + from sqa_utils.types.choice import EnumTypeImpl except ImportError: EnumTypeImpl = object @@ -200,13 +199,16 @@ def convert_sqlalchemy_type(type, column, registry=None): @convert_sqlalchemy_type.register(sqa_types.UnicodeText) @convert_sqlalchemy_type.register(postgresql.INET) @convert_sqlalchemy_type.register(postgresql.CIDR) -@convert_sqlalchemy_type.register(TSVectorType) +@convert_sqlalchemy_type.register(sqa_utils.TSVectorType) +@convert_sqlalchemy_type.register(sqa_utils.EmailType) +@convert_sqlalchemy_type.register(sqa_utils.URLType) +@convert_sqlalchemy_type.register(sqa_utils.IPAddressType) def convert_column_to_string(type, column, registry=None): return graphene.String @convert_sqlalchemy_type.register(postgresql.UUID) -@convert_sqlalchemy_type.register(UUIDType) +@convert_sqlalchemy_type.register(sqa_utils.UUIDType) def convert_column_to_uuid(type, column, registry=None): return graphene.UUID @@ -251,7 +253,7 @@ def convert_enum_to_enum(type, column, registry=None): # TODO Make ChoiceType conversion consistent with other enums -@convert_sqlalchemy_type.register(ChoiceType) +@convert_sqlalchemy_type.register(sqa_utils.ChoiceType) def convert_choice_to_enum(type, column, registry=None): name = "{}_{}".format(column.table.name, column.key).upper() if isinstance(type.type_impl, EnumTypeImpl): @@ -262,7 +264,7 @@ def convert_choice_to_enum(type, column, registry=None): return graphene.Enum(name, type.choices) -@convert_sqlalchemy_type.register(ScalarListType) +@convert_sqlalchemy_type.register(sqa_utils.ScalarListType) def convert_scalar_list_to_list(type, column, registry=None): return graphene.List(graphene.String) @@ -285,7 +287,7 @@ def convert_json_to_string(type, column, registry=None): return JSONString -@convert_sqlalchemy_type.register(JSONType) +@convert_sqlalchemy_type.register(sqa_utils.JSONType) @convert_sqlalchemy_type.register(sqa_types.JSON) def convert_json_type_to_string(type, column, registry=None): return JSONString diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 47527c12..2d086225 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -9,14 +9,12 @@ from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.inspection import inspect from sqlalchemy.orm import column_property, composite -from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, UUIDType +import sqlalchemy_utils as sqa_utils -import graphene -from graphene import Boolean, Float, Int, Scalar, String from graphene.relay import Node -from graphene.types.datetime import Date, DateTime, Time -from graphene.types.json import JSONString -from graphene.types.structures import List, Structure +import graphene +from graphene.types.structures import Structure + from ..converter import (convert_sqlalchemy_column, convert_sqlalchemy_composite, @@ -187,7 +185,7 @@ def test_should_numeric_convert_float(): def test_should_choice_convert_enum(): - field = get_field(ChoiceType([(u"es", u"Spanish"), (u"en", u"English")])) + field = get_field(sqa_utils.ChoiceType([(u"es", u"Spanish"), (u"en", u"English")])) graphene_type = field.type assert issubclass(graphene_type, graphene.Enum) assert graphene_type._meta.name == "MODEL_COLUMN" @@ -200,7 +198,7 @@ class TestEnum(enum.Enum): es = u"Spanish" en = u"English" - field = get_field(ChoiceType(TestEnum, impl=types.String())) + field = get_field(sqa_utils.ChoiceType(TestEnum, impl=types.String())) graphene_type = field.type assert issubclass(graphene_type, graphene.Enum) assert graphene_type._meta.name == "MODEL_COLUMN" @@ -218,7 +216,7 @@ class TestEnum(enum.Enum): es = u"Spanish" en = u"English" - testChoice = Column("% descuento1", ChoiceType(TestEnum, impl=types.String()), key="descuento1") + testChoice = Column("% descuento1", sqa_utils.ChoiceType(TestEnum, impl=types.String()), key="descuento1") field = get_field_from_column(testChoice) graphene_type = field.type @@ -233,7 +231,7 @@ class TestEnum(enum.IntEnum): one = 1 two = 2 - field = get_field(ChoiceType(TestEnum, impl=types.String())) + field = get_field(sqa_utils.ChoiceType(TestEnum, impl=types.String())) graphene_type = field.type assert issubclass(graphene_type, graphene.Enum) assert graphene_type._meta.name == "MODEL_COLUMN" @@ -250,14 +248,14 @@ def test_should_columproperty_convert(): def test_should_scalar_list_convert_list(): - field = get_field(ScalarListType()) + field = get_field(sqa_utils.ScalarListType()) assert isinstance(field.type, graphene.List) assert field.type.of_type == graphene.String def test_should_jsontype_convert_jsonstring(): - assert get_field(JSONType()).type == JSONString - assert get_field(types.JSON).type == JSONString + assert get_field(sqa_utils.JSONType()).type == graphene.JSONString + assert get_field(types.JSON).type == graphene.JSONString def test_should_variant_int_convert_int(): @@ -369,7 +367,7 @@ def test_should_postgresql_uuid_convert(): def test_should_sqlalchemy_utils_uuid_convert(): - assert get_field(UUIDType()).type == graphene.UUID + assert get_field(sqa_utils.UUIDType()).type == graphene.UUID def test_should_postgresql_enum_convert(): @@ -483,8 +481,8 @@ class Meta: # Check ShoppingCartItem's Properties and Return Types ####################################################### - shopping_cart_item_expected_types: Dict[str, Union[Scalar, Structure]] = { - 'hybrid_prop_shopping_cart': List(ShoppingCartType) + shopping_cart_item_expected_types: Dict[str, Union[graphene.Scalar, Structure]] = { + 'hybrid_prop_shopping_cart': graphene.List(ShoppingCartType) } assert sorted(list(ShoppingCartItemType._meta.fields.keys())) == sorted([ @@ -509,27 +507,27 @@ class Meta: # Check ShoppingCart's Properties and Return Types ################################################### - shopping_cart_expected_types: Dict[str, Union[Scalar, Structure]] = { + shopping_cart_expected_types: Dict[str, Union[graphene.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, + "hybrid_prop_str": graphene.String, + "hybrid_prop_int": graphene.Int, + "hybrid_prop_float": graphene.Float, + "hybrid_prop_bool": graphene.Boolean, + "hybrid_prop_decimal": graphene.String, # Decimals should be serialized Strings + "hybrid_prop_date": graphene.Date, + "hybrid_prop_time": graphene.Time, + "hybrid_prop_datetime": graphene.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_list_int": graphene.List(graphene.Int), + "hybrid_prop_list_date": graphene.List(graphene.Date), + "hybrid_prop_nested_list_int": graphene.List(graphene.List(graphene.Int)), + "hybrid_prop_deeply_nested_list_int": graphene.List(graphene.List(graphene.List(graphene.Int))), "hybrid_prop_first_shopping_cart_item": ShoppingCartItemType, - "hybrid_prop_shopping_cart_item_list": List(ShoppingCartItemType), - "hybrid_prop_unsupported_type_tuple": String, + "hybrid_prop_shopping_cart_item_list": graphene.List(ShoppingCartItemType), + "hybrid_prop_unsupported_type_tuple": graphene.String, # Self Referential List "hybrid_prop_self_referential": ShoppingCartType, - "hybrid_prop_self_referential_list": List(ShoppingCartType), + "hybrid_prop_self_referential_list": graphene.List(ShoppingCartType), # Optionals "hybrid_prop_optional_self_referential": ShoppingCartType, } From 506bb30647808973b8fbff7db971291fe9840b1f Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Thu, 23 Jun 2022 19:56:22 +0200 Subject: [PATCH 12/15] Add DummyImport for sqlalchemy_utils Signed-off-by: Erik Wrede --- graphene_sqlalchemy/tests/test_utils.py | 6 +++++- graphene_sqlalchemy/utils.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/tests/test_utils.py b/graphene_sqlalchemy/tests/test_utils.py index e13d919c..53a19eaa 100644 --- a/graphene_sqlalchemy/tests/test_utils.py +++ b/graphene_sqlalchemy/tests/test_utils.py @@ -4,7 +4,7 @@ from graphene import Enum, List, ObjectType, Schema, String from ..utils import (get_session, sort_argument_for_model, sort_enum_for_model, - to_enum_value_name, to_type_name) + to_enum_value_name, to_type_name, DummyImport) from .models import Base, Editor, Pet @@ -99,3 +99,7 @@ class MultiplePK(Base): assert set(arg.default_value) == set( (MultiplePK.foo.name + "_asc", MultiplePK.bar.name + "_asc") ) + +def test_dummy_import(): + dummy_module = DummyImport() + assert dummy_module.foo == object diff --git a/graphene_sqlalchemy/utils.py b/graphene_sqlalchemy/utils.py index e02b1439..f6ee9b62 100644 --- a/graphene_sqlalchemy/utils.py +++ b/graphene_sqlalchemy/utils.py @@ -206,3 +206,9 @@ def registry_sqlalchemy_model_from_str(model_name: str) -> Optional[Any]: return next(filter(lambda x: x.__name__ == model_name, list(get_global_registry()._registry.keys()))) except StopIteration: pass + + +class DummyImport: + """The dummy module returns 'object' for a query for any member""" + def __getattr__(self, name): + return object From bc90e57c157c0a482f5c05ac8c1687d8bec7e787 Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Mon, 27 Jun 2022 18:00:49 +0200 Subject: [PATCH 13/15] Fix pytest Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index f260861e..0324775d 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -28,14 +28,14 @@ from typing import _ForwardRef as ForwardRef try: - import sqlalchemy_utils as sqa_utils + from sqlalchemy_utils.types.choice import EnumTypeImpl except ImportError: - sqlalchemy_utils = DummyImport() + EnumTypeImpl = object try: - from sqa_utils.types.choice import EnumTypeImpl + import sqlalchemy_utils as sqa_utils except ImportError: - EnumTypeImpl = object + sqa_utils = DummyImport() is_selectin_available = getattr(strategies, 'SelectInLoader', None) @@ -257,6 +257,8 @@ def convert_enum_to_enum(type, column, registry=None): def convert_choice_to_enum(type, column, registry=None): name = "{}_{}".format(column.table.name, column.key).upper() if isinstance(type.type_impl, EnumTypeImpl): + print("AAAA") + print(EnumTypeImpl) # type.choices may be Enum/IntEnum, in ChoiceType both presented as EnumMeta # do not use from_enum here because we can have more than one enum column in table return graphene.Enum(name, list((v.name, v.value) for v in type.choices)) @@ -410,7 +412,8 @@ def convert_sqlalchemy_hybrid_property_union(arg): "Please add the corresponding hybrid_property to the excluded fields in the ObjectType, " "or use an ORMField to override this behaviour.") - return graphene_union_for_py_union(cast(typing.List[graphene.ObjectType], list(graphene_types)), get_global_registry()) + return graphene_union_for_py_union(cast(typing.List[graphene.ObjectType], list(graphene_types)), + get_global_registry()) @convert_sqlalchemy_hybrid_property_type.register(lambda x: getattr(x, '__origin__', None) in [list, typing.List]) From 91762db4c386300c20374d282d245f14df6e0c7f Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Tue, 28 Jun 2022 15:48:17 +0200 Subject: [PATCH 14/15] Improve test coverage Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 8 +- graphene_sqlalchemy/registry.py | 12 +-- graphene_sqlalchemy/tests/test_converter.py | 87 +++++++++++++++++++-- graphene_sqlalchemy/tests/test_registry.py | 56 ++++++++++++- 4 files changed, 149 insertions(+), 14 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 0324775d..1b3532a5 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -307,6 +307,12 @@ def convert_sqlalchemy_hybrid_property_type(arg: Any): if existing_graphql_type: return existing_graphql_type + if isinstance(arg, type(graphene.ObjectType)): + return arg + + if isinstance(arg, type(graphene.Scalar)): + return arg + # 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." @@ -407,7 +413,7 @@ def convert_sqlalchemy_hybrid_property_union(arg): return graphene_types[0] # Now check if every type is instance of an ObjectType - if not all(isinstance(graphene_type, graphene.ObjectType) for graphene_type in graphene_types): + if not all(isinstance(graphene_type, type(graphene.ObjectType)) for graphene_type in graphene_types): raise ValueError("Cannot convert hybrid_property Union to graphene.Union: the Union contains scalars. " "Please add the corresponding hybrid_property to the excluded fields in the ObjectType, " "or use an ORMField to override this behaviour.") diff --git a/graphene_sqlalchemy/registry.py b/graphene_sqlalchemy/registry.py index bf0f31a1..80470d9b 100644 --- a/graphene_sqlalchemy/registry.py +++ b/graphene_sqlalchemy/registry.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import List +from typing import List, Type from sqlalchemy.types import Enum as SQLAlchemyEnumType @@ -89,22 +89,22 @@ def register_sort_enum(self, obj_type, sort_enum: Enum): def get_sort_enum_for_object_type(self, obj_type: graphene.ObjectType): return self._registry_sort_enums.get(obj_type) - def register_union_type(self, union: graphene.Union, obj_types: List[graphene.ObjectType]): + def register_union_type(self, union: graphene.Union, obj_types: List[Type[graphene.ObjectType]]): if not isinstance(union, graphene.Union): raise TypeError( "Expected graphene.Union, but got: {!r}".format(union) ) for obj_type in obj_types: - if not isinstance(obj_type, graphene.ObjectType): + if not isinstance(obj_type, type(graphene.ObjectType)): raise TypeError( "Expected Graphene ObjectType, but got: {!r}".format(obj_type) ) - self._registry_enums[tuple(sorted(obj_types))] = union + self._registry_unions[frozenset(obj_types)] = union - def get_union_for_object_types(self, obj_types: List[graphene.ObjectType]): - self._registry_unions.get(tuple(sorted(obj_types))) + def get_union_for_object_types(self, obj_types : List[Type[graphene.ObjectType]]): + return self._registry_unions.get(frozenset(obj_types)) registry = None diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 2d086225..126bd53f 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -15,7 +15,6 @@ import graphene from graphene.types.structures import Structure - from ..converter import (convert_sqlalchemy_column, convert_sqlalchemy_composite, convert_sqlalchemy_hybrid_method, @@ -94,11 +93,63 @@ def prop_method() -> int | None: @pytest.mark.skipif(sys.version_info < (3, 10), reason="|-Style Unions are unsupported in python < 3.10") -def test_should_unknown_sqlalchemy_field_raise_exception_310(): - re_err = "Don't know how to convert the SQLAlchemy field" - with pytest.raises(Exception, match=re_err): - # support legacy Binary type and subsequent LargeBinary - get_field(getattr(types, 'LargeBinary', types.BINARY)()) +def test_should_union_work_310(): + reg = Registry() + + class PetType(SQLAlchemyObjectType): + class Meta: + model = Pet + registry = reg + + class ShoppingCartType(SQLAlchemyObjectType): + class Meta: + model = ShoppingCartItem + registry = reg + + @hybrid_property + def prop_method() -> Union[PetType, ShoppingCartType]: + return None + + @hybrid_property + def prop_method_2() -> Union[ShoppingCartType, PetType]: + return None + + field_type_1 = get_hybrid_property_type(prop_method).type + field_type_2 = get_hybrid_property_type(prop_method_2).type + + assert isinstance(field_type_1, graphene.Union) + assert field_type_1 is field_type_2 + + # TODO verify types of the union + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="|-Style Unions are unsupported in python < 3.10") +def test_should_union_work_310(): + reg = Registry() + + class PetType(SQLAlchemyObjectType): + class Meta: + model = Pet + registry = reg + + class ShoppingCartType(SQLAlchemyObjectType): + class Meta: + model = ShoppingCartItem + registry = reg + + @hybrid_property + def prop_method() -> PetType | ShoppingCartType: + return None + + @hybrid_property + def prop_method_2() -> ShoppingCartType | PetType: + return None + + field_type_1 = get_hybrid_property_type(prop_method).type + field_type_2 = get_hybrid_property_type(prop_method_2).type + + assert isinstance(field_type_1, graphene.Union) + assert field_type_1 is field_type_2 def test_should_datetime_convert_datetime(): @@ -129,6 +180,30 @@ def test_should_unicodetext_convert_string(): assert get_field(types.UnicodeText()).type == graphene.String +def test_should_tsvector_convert_string(): + assert get_field(sqa_utils.TSVectorType()).type == graphene.String + + +def test_should_email_convert_string(): + assert get_field(sqa_utils.EmailType()).type == graphene.String + + +def test_should_URL_convert_string(): + assert get_field(sqa_utils.URLType()).type == graphene.String + + +def test_should_IPaddress_convert_string(): + assert get_field(sqa_utils.IPAddressType()).type == graphene.String + + +def test_should_inet_convert_string(): + assert get_field(postgresql.INET()).type == graphene.String + + +def test_should_cidr_convert_string(): + assert get_field(postgresql.CIDR()).type == graphene.String + + def test_should_enum_convert_enum(): field = get_field(types.Enum(enum.Enum("TwoNumbers", ("one", "two")))) field_type = field.type() diff --git a/graphene_sqlalchemy/tests/test_registry.py b/graphene_sqlalchemy/tests/test_registry.py index 0403c4f0..0db0916e 100644 --- a/graphene_sqlalchemy/tests/test_registry.py +++ b/graphene_sqlalchemy/tests/test_registry.py @@ -2,11 +2,12 @@ from sqlalchemy.types import Enum as SQLAlchemyEnum from graphene import Enum as GrapheneEnum +import graphene from ..registry import Registry from ..types import SQLAlchemyObjectType from ..utils import EnumValue -from .models import Pet +from .models import Pet, Reporter def test_register_object_type(): @@ -126,3 +127,56 @@ class Meta: re_err = r"Expected Graphene Enum, but got: .*PetType.*" with pytest.raises(TypeError, match=re_err): reg.register_sort_enum(PetType, PetType) + + +def test_register_union(): + reg = Registry() + + class PetType(SQLAlchemyObjectType): + class Meta: + model = Pet + registry = reg + + class ReporterType(SQLAlchemyObjectType): + class Meta: + model = Reporter + + union_types = [PetType, ReporterType] + union = graphene.Union('ReporterPet', tuple(union_types)) + + reg.register_union_type(union, union_types) + + assert reg.get_union_for_object_types(union_types) == union + # Order should no matter + assert reg.get_union_for_object_types([ReporterType, PetType]) == union + + +def test_register_union_scalar(): + reg = Registry() + + union_types = [graphene.String, graphene.Int] + union = graphene.Union('StringInt', tuple(union_types)) + + re_err = r"Expected Graphene ObjectType, but got: .*String.*" + with pytest.raises(TypeError, match=re_err): + reg.register_union_type(union, union_types) + + +def test_register_union_incorrect_types(): + reg = Registry() + + class PetType(SQLAlchemyObjectType): + class Meta: + model = Pet + registry = reg + + class ReporterType(SQLAlchemyObjectType): + class Meta: + model = Reporter + + union_types = [PetType, ReporterType] + union = PetType + + re_err = r"Expected graphene.Union, but got: .*PetType.*" + with pytest.raises(TypeError, match=re_err): + reg.register_union_type(union, union_types) From 195b29f63a29bb09578d2bb35853206cb747183d Mon Sep 17 00:00:00 2001 From: Erik Wrede Date: Tue, 12 Jul 2022 19:52:15 +0200 Subject: [PATCH 15/15] Review+isort Signed-off-by: Erik Wrede --- graphene_sqlalchemy/converter.py | 10 ++++------ graphene_sqlalchemy/tests/test_converter.py | 4 ++-- graphene_sqlalchemy/tests/test_registry.py | 4 ++-- graphene_sqlalchemy/tests/test_utils.py | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 1b3532a5..1e7846eb 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -17,9 +17,11 @@ from .enums import enum_for_sa_enum from .fields import (BatchSQLAlchemyConnectionField, default_connection_field_factory) +from .registry import get_global_registry from .resolvers import get_attr_resolver, get_custom_resolver -from .utils import (registry_sqlalchemy_model_from_str, safe_isinstance, - singledispatchbymatchfunction, value_equals, DummyImport) +from .utils import (DummyImport, registry_sqlalchemy_model_from_str, + safe_isinstance, singledispatchbymatchfunction, + value_equals) try: from typing import ForwardRef @@ -248,7 +250,6 @@ def convert_column_to_float(type, column, registry=None): @convert_sqlalchemy_type.register(sqa_types.Enum) def convert_enum_to_enum(type, column, registry=None): - from .registry import get_global_registry return lambda: enum_for_sa_enum(type, registry or get_global_registry()) @@ -257,8 +258,6 @@ def convert_enum_to_enum(type, column, registry=None): def convert_choice_to_enum(type, column, registry=None): name = "{}_{}".format(column.table.name, column.key).upper() if isinstance(type.type_impl, EnumTypeImpl): - print("AAAA") - print(EnumTypeImpl) # type.choices may be Enum/IntEnum, in ChoiceType both presented as EnumMeta # do not use from_enum here because we can have more than one enum column in table return graphene.Enum(name, list((v.name, v.value) for v in type.choices)) @@ -302,7 +301,6 @@ def convert_variant_to_impl_type(type, column, registry=None): @singledispatchbymatchfunction def convert_sqlalchemy_hybrid_property_type(arg: Any): - from .registry import get_global_registry existing_graphql_type = get_global_registry().get_type_for_model(arg) if existing_graphql_type: return existing_graphql_type diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 126bd53f..a6c2b1bf 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -3,16 +3,16 @@ from typing import Dict, Union import pytest +import sqlalchemy_utils as sqa_utils from sqlalchemy import Column, func, select, types from sqlalchemy.dialects import postgresql from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.inspection import inspect from sqlalchemy.orm import column_property, composite -import sqlalchemy_utils as sqa_utils -from graphene.relay import Node import graphene +from graphene.relay import Node from graphene.types.structures import Structure from ..converter import (convert_sqlalchemy_column, diff --git a/graphene_sqlalchemy/tests/test_registry.py b/graphene_sqlalchemy/tests/test_registry.py index 0db0916e..f451f355 100644 --- a/graphene_sqlalchemy/tests/test_registry.py +++ b/graphene_sqlalchemy/tests/test_registry.py @@ -1,8 +1,8 @@ import pytest from sqlalchemy.types import Enum as SQLAlchemyEnum -from graphene import Enum as GrapheneEnum import graphene +from graphene import Enum as GrapheneEnum from ..registry import Registry from ..types import SQLAlchemyObjectType @@ -147,7 +147,7 @@ class Meta: reg.register_union_type(union, union_types) assert reg.get_union_for_object_types(union_types) == union - # Order should no matter + # Order should not matter assert reg.get_union_for_object_types([ReporterType, PetType]) == union diff --git a/graphene_sqlalchemy/tests/test_utils.py b/graphene_sqlalchemy/tests/test_utils.py index 53a19eaa..de359e05 100644 --- a/graphene_sqlalchemy/tests/test_utils.py +++ b/graphene_sqlalchemy/tests/test_utils.py @@ -3,8 +3,8 @@ from graphene import Enum, List, ObjectType, Schema, String -from ..utils import (get_session, sort_argument_for_model, sort_enum_for_model, - to_enum_value_name, to_type_name, DummyImport) +from ..utils import (DummyImport, get_session, sort_argument_for_model, + sort_enum_for_model, to_enum_value_name, to_type_name) from .models import Base, Editor, Pet