Skip to content

Commit 1708fcf

Browse files
authored
fix: allow type converter inheritance again (#377)
* fix: Make ORMField(type_) work in case there is no registered converter * revert/feat!: Type Converters support subtypes again. this feature adjusts the conversion system to use the MRO of a supplied class * tests: add test cases for mro & orm field fixes * tests: use custom type instead of BIGINT due to version incompatibilities
1 parent d3a4320 commit 1708fcf

File tree

4 files changed

+98
-12
lines changed

4 files changed

+98
-12
lines changed

Diff for: graphene_sqlalchemy/converter.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -261,16 +261,17 @@ def inner(fn):
261261

262262
def convert_sqlalchemy_column(column_prop, registry, resolver, **field_kwargs):
263263
column = column_prop.columns[0]
264-
column_type = getattr(column, "type", None)
265264
# The converter expects a type to find the right conversion function.
266265
# If we get an instance instead, we need to convert it to a type.
267266
# The conversion function will still be able to access the instance via the column argument.
268-
if not isinstance(column_type, type):
269-
column_type = type(column_type)
270-
field_kwargs.setdefault(
271-
"type_",
272-
convert_sqlalchemy_type(column_type, column=column, registry=registry),
273-
)
267+
if "type_" not in field_kwargs:
268+
column_type = getattr(column, "type", None)
269+
if not isinstance(column_type, type):
270+
column_type = type(column_type)
271+
field_kwargs.setdefault(
272+
"type_",
273+
convert_sqlalchemy_type(column_type, column=column, registry=registry),
274+
)
274275
field_kwargs.setdefault("required", not is_column_nullable(column))
275276
field_kwargs.setdefault("description", get_column_doc(column))
276277

Diff for: graphene_sqlalchemy/tests/models.py

+38
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from sqlalchemy.ext.declarative import declarative_base
2222
from sqlalchemy.ext.hybrid import hybrid_property
2323
from sqlalchemy.orm import backref, column_property, composite, mapper, relationship
24+
from sqlalchemy.sql.sqltypes import _LookupExpressionAdapter
25+
from sqlalchemy.sql.type_api import TypeEngine
2426

2527
PetKind = Enum("cat", "dog", name="pet_kind")
2628

@@ -328,3 +330,39 @@ class Employee(Person):
328330
__mapper_args__ = {
329331
"polymorphic_identity": "employee",
330332
}
333+
334+
335+
############################################
336+
# Custom Test Models
337+
############################################
338+
339+
340+
class CustomIntegerColumn(_LookupExpressionAdapter, TypeEngine):
341+
"""
342+
Custom Column Type that our converters don't recognize
343+
Adapted from sqlalchemy.Integer
344+
"""
345+
346+
"""A type for ``int`` integers."""
347+
348+
__visit_name__ = "integer"
349+
350+
def get_dbapi_type(self, dbapi):
351+
return dbapi.NUMBER
352+
353+
@property
354+
def python_type(self):
355+
return int
356+
357+
def literal_processor(self, dialect):
358+
def process(value):
359+
return str(int(value))
360+
361+
return process
362+
363+
364+
class CustomColumnModel(Base):
365+
__tablename__ = "customcolumnmodel"
366+
367+
id = Column(Integer(), primary_key=True)
368+
custom_col = Column(CustomIntegerColumn)

Diff for: graphene_sqlalchemy/tests/test_converter.py

+38-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Dict, Tuple, Union
44

55
import pytest
6+
import sqlalchemy
67
import sqlalchemy_utils as sqa_utils
78
from sqlalchemy import Column, func, select, types
89
from sqlalchemy.dialects import postgresql
@@ -29,6 +30,7 @@
2930
from .models import (
3031
Article,
3132
CompositeFullName,
33+
CustomColumnModel,
3234
Pet,
3335
Reporter,
3436
ShoppingCart,
@@ -81,7 +83,6 @@ def use_legacy_many_relationships():
8183
set_non_null_many_relationships(True)
8284

8385

84-
8586
def test_hybrid_prop_int():
8687
@hybrid_property
8788
def prop_method() -> int:
@@ -745,6 +746,42 @@ def __init__(self, col1, col2):
745746
)
746747

747748

749+
def test_raise_exception_unkown_column_type():
750+
with pytest.raises(
751+
Exception,
752+
match="Don't know how to convert the SQLAlchemy field customcolumnmodel.custom_col",
753+
):
754+
755+
class A(SQLAlchemyObjectType):
756+
class Meta:
757+
model = CustomColumnModel
758+
759+
760+
def test_prioritize_orm_field_unkown_column_type():
761+
class A(SQLAlchemyObjectType):
762+
class Meta:
763+
model = CustomColumnModel
764+
765+
custom_col = ORMField(type_=graphene.Int)
766+
767+
assert A._meta.fields["custom_col"].type == graphene.Int
768+
769+
770+
def test_match_supertype_from_mro_correct_order():
771+
"""
772+
BigInt and Integer are both superclasses of BIGINT, but a custom converter exists for BigInt that maps to Float.
773+
We expect the correct MRO order to be used and conversion by the nearest match. BIGINT should be converted to Float,
774+
just like BigInt, not to Int like integer which is further up in the MRO.
775+
"""
776+
777+
class BIGINT(sqlalchemy.types.BigInteger):
778+
pass
779+
780+
field = get_field_from_column(Column(BIGINT))
781+
782+
assert field.type == graphene.Float
783+
784+
748785
def test_sqlalchemy_hybrid_property_type_inference():
749786
class ShoppingCartItemType(SQLAlchemyObjectType):
750787
class Meta:

Diff for: graphene_sqlalchemy/utils.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
import warnings
33
from collections import OrderedDict
4+
from functools import _c3_mro
45
from typing import Any, Callable, Dict, Optional
56

67
import pkg_resources
@@ -188,10 +189,19 @@ def __init__(self, default: Callable):
188189
self.default = default
189190

190191
def __call__(self, *args, **kwargs):
191-
for matcher_function, final_method in self.registry.items():
192-
# Register order is important. First one that matches, runs.
193-
if matcher_function(args[0]):
194-
return final_method(*args, **kwargs)
192+
matched_arg = args[0]
193+
try:
194+
mro = _c3_mro(matched_arg)
195+
except Exception:
196+
# In case of tuples or similar types, we can't use the MRO.
197+
# Fall back to just matching the original argument.
198+
mro = [matched_arg]
199+
200+
for cls in mro:
201+
for matcher_function, final_method in self.registry.items():
202+
# Register order is important. First one that matches, runs.
203+
if matcher_function(cls):
204+
return final_method(*args, **kwargs)
195205

196206
# No match, using default.
197207
return self.default(*args, **kwargs)

0 commit comments

Comments
 (0)