Skip to content

Commit c57eada

Browse files
committed
New test and models specific to hybrid_property type inference tests.
1 parent 95323f5 commit c57eada

File tree

3 files changed

+135
-33
lines changed

3 files changed

+135
-33
lines changed

graphene_sqlalchemy/converter.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import datetime
2-
import enum
32
import typing
43
import warnings
54
from decimal import Decimal
@@ -20,8 +19,7 @@
2019
default_connection_field_factory)
2120
from .registry import get_global_registry
2221
from .resolvers import get_attr_resolver, get_custom_resolver
23-
from .utils import (singledispatchbymatchfunction, value_equals,
24-
value_is_subclass)
22+
from .utils import singledispatchbymatchfunction, value_equals
2523

2624
try:
2725
from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType, TSVectorType
@@ -326,11 +324,6 @@ def convert_sqlalchemy_hybrid_property_type_time(arg):
326324
return Time
327325

328326

329-
@convert_sqlalchemy_hybrid_property_type.register(value_is_subclass(enum.Enum))
330-
def convert_sqlalchemy_hybrid_property_type_enum(arg):
331-
return Enum.from_enum(arg)
332-
333-
334327
@convert_sqlalchemy_hybrid_property_type.register(lambda x: getattr(x, '__origin__', None) in [list, typing.List])
335328
def convert_sqlalchemy_hybrid_property_type_list_t(arg):
336329
# type is either list[T] or List[T], generic argument at __args__[0]

graphene_sqlalchemy/tests/models.py

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class Reporter(Base):
7777
favorite_article = relationship("Article", uselist=False)
7878

7979
@hybrid_property
80-
def hybrid_prop_untyped(self):
80+
def hybrid_prop(self):
8181
return self.first_name
8282

8383
@hybrid_property
@@ -97,48 +97,104 @@ def hybrid_prop_bool(self) -> bool:
9797
return True
9898

9999
@hybrid_property
100-
def hybrid_prop_list_int(self) -> List[int]:
100+
def hybrid_prop_list(self) -> List[int]:
101101
return [1, 2, 3]
102102

103+
column_prop = column_property(
104+
select([func.cast(func.count(id), Integer)]), doc="Column property"
105+
)
106+
107+
composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite")
108+
109+
110+
class ReflectedEditor(type):
111+
"""Same as Editor, but using reflected table."""
112+
113+
@classmethod
114+
def __subclasses__(cls):
115+
return []
116+
117+
118+
editor_table = Table("editors", Base.metadata, autoload=True)
119+
120+
mapper(ReflectedEditor, editor_table)
121+
122+
123+
############################################
124+
# The models below are mainly used in the
125+
# @hybrid_property type inference scenarios
126+
############################################
127+
128+
129+
class ShoppingCartItem(Base):
130+
__tablename__ = "shopping_cart_items"
131+
132+
id = Column(Integer(), primary_key=True)
133+
134+
135+
class ShoppingCart(Base):
136+
__tablename__ = "shopping_carts"
137+
138+
id = Column(Integer(), primary_key=True)
139+
140+
# Standard Library types
141+
103142
@hybrid_property
104-
def hybrid_prop_list_date(self) -> List[datetime.date]:
105-
return [self.hybrid_prop_date, self.hybrid_prop_date, self.hybrid_prop_date]
143+
def hybrid_prop_str(self) -> str:
144+
return self.first_name
106145

107146
@hybrid_property
108-
def hybrid_prop_date(self) -> datetime.date:
109-
return datetime.datetime.now().date()
147+
def hybrid_prop_int(self) -> int:
148+
return 42
110149

111150
@hybrid_property
112-
def hybrid_prop_time(self) -> datetime.time:
113-
return datetime.datetime.now().time()
151+
def hybrid_prop_float(self) -> float:
152+
return 42.3
114153

115154
@hybrid_property
116-
def hybrid_prop_datetime(self) -> datetime.datetime:
117-
return datetime.datetime.now()
155+
def hybrid_prop_bool(self) -> bool:
156+
return True
118157

119158
@hybrid_property
120159
def hybrid_prop_decimal(self) -> Decimal:
121160
return Decimal("3.14")
122161

123162
@hybrid_property
124-
def hybrid_prop_first_article(self) -> Article:
125-
return self.articles[0]
163+
def hybrid_prop_date(self) -> datetime.date:
164+
return datetime.datetime.now().date()
126165

127-
column_prop = column_property(
128-
select([func.cast(func.count(id), Integer)]), doc="Column property"
129-
)
166+
@hybrid_property
167+
def hybrid_prop_time(self) -> datetime.time:
168+
return datetime.datetime.now().time()
130169

131-
composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite")
170+
@hybrid_property
171+
def hybrid_prop_datetime(self) -> datetime.datetime:
172+
return datetime.datetime.now()
132173

174+
# Lists and Nested Lists
133175

134-
class ReflectedEditor(type):
135-
"""Same as Editor, but using reflected table."""
176+
@hybrid_property
177+
def hybrid_prop_list_int(self) -> List[int]:
178+
return [1, 2, 3]
136179

137-
@classmethod
138-
def __subclasses__(cls):
139-
return []
180+
@hybrid_property
181+
def hybrid_prop_list_date(self) -> List[datetime.date]:
182+
return [self.hybrid_prop_date, self.hybrid_prop_date, self.hybrid_prop_date]
140183

184+
@hybrid_property
185+
def hybrid_prop_nested_list_int(self) -> List[List[int]]:
186+
return [self.hybrid_prop_list_int, ]
141187

142-
editor_table = Table("editors", Base.metadata, autoload=True)
188+
@hybrid_property
189+
def hybrid_prop_deeply_nested_list_int(self) -> List[List[List[int]]]:
190+
return [[self.hybrid_prop_list_int, ], ]
143191

144-
mapper(ReflectedEditor, editor_table)
192+
# Other SQLAlchemy Instances
193+
@hybrid_property
194+
def hybrid_prop_first_shopping_cart_item(self) -> ShoppingCartItem:
195+
return ShoppingCartItem(id=1)
196+
197+
# Other SQLAlchemy Instances
198+
@hybrid_property
199+
def hybrid_prop_shopping_cart_item_list(self) -> List[ShoppingCartItem]:
200+
return [ShoppingCartItem(id=1), ShoppingCartItem(id=2)]

graphene_sqlalchemy/tests/test_converter.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import enum
2+
from typing import Dict, Union
23

34
import pytest
45
from sqlalchemy import Column, func, select, types
@@ -9,9 +10,11 @@
910
from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType
1011

1112
import graphene
13+
from graphene import Boolean, Float, Int, Scalar, String
1214
from graphene.relay import Node
13-
from graphene.types.datetime import DateTime
15+
from graphene.types.datetime import Date, DateTime, Time
1416
from graphene.types.json import JSONString
17+
from graphene.types.structures import List, Structure
1518

1619
from ..converter import (convert_sqlalchemy_column,
1720
convert_sqlalchemy_composite,
@@ -20,7 +23,8 @@
2023
default_connection_field_factory)
2124
from ..registry import Registry, get_global_registry
2225
from ..types import SQLAlchemyObjectType
23-
from .models import Article, CompositeFullName, Pet, Reporter
26+
from .models import (Article, CompositeFullName, Pet, Reporter, ShoppingCart,
27+
ShoppingCartItem)
2428

2529

2630
def mock_resolver():
@@ -384,3 +388,52 @@ def __init__(self, col1, col2):
384388
Registry(),
385389
mock_resolver,
386390
)
391+
392+
393+
def test_sqlalchemy_hybrid_property_type_inference():
394+
class ShoppingCartItemType(SQLAlchemyObjectType):
395+
class Meta:
396+
model = ShoppingCartItem
397+
interfaces = (Node,)
398+
399+
class ShoppingCartType(SQLAlchemyObjectType):
400+
class Meta:
401+
model = ShoppingCart
402+
interfaces = (Node,)
403+
404+
hybrid_prop_expected_types: Dict[str, Union[Scalar, Structure]] = {
405+
# Basic types
406+
"hybrid_prop_str": String,
407+
"hybrid_prop_int": Int,
408+
"hybrid_prop_float": Float,
409+
"hybrid_prop_bool": Boolean,
410+
"hybrid_prop_decimal": String, # Decimals should be serialized Strings
411+
"hybrid_prop_date": Date,
412+
"hybrid_prop_time": Time,
413+
"hybrid_prop_datetime": DateTime,
414+
# Lists and Nested Lists
415+
"hybrid_prop_list_int": List(Int),
416+
"hybrid_prop_list_date": List(Date),
417+
"hybrid_prop_nested_list_int": List(List(Int)),
418+
"hybrid_prop_deeply_nested_list_int": List(List(List(Int))),
419+
"hybrid_prop_first_shopping_cart_item": ShoppingCartItemType,
420+
"hybrid_prop_shopping_cart_item_list": List(ShoppingCartItemType)
421+
}
422+
423+
assert sorted(list(ShoppingCartType._meta.fields.keys())) == sorted([
424+
# Columns
425+
"id",
426+
# Append Hybrid Properties from Above
427+
*hybrid_prop_expected_types.keys()
428+
])
429+
430+
for hybrid_prop_name, hybrid_prop_expected_return_type in hybrid_prop_expected_types.items():
431+
hybrid_prop_field = ShoppingCartType._meta.fields[hybrid_prop_name]
432+
433+
# this is a simple way of showing the failed property name
434+
# instead of having to unroll the loop.
435+
assert (
436+
(hybrid_prop_name, str(hybrid_prop_field.type)) ==
437+
(hybrid_prop_name, str(hybrid_prop_expected_return_type))
438+
)
439+
assert hybrid_prop_field.description is None # "doc" is ignored by hybrid property

0 commit comments

Comments
 (0)