Skip to content

Commit d0668cc

Browse files
authored
feat: SQLAlchemy 2.0 support (#368)
This PR updates the dataloader and unit tests to be compatible with sqlalchemy 2.0
1 parent 882205d commit d0668cc

File tree

10 files changed

+100
-31
lines changed

10 files changed

+100
-31
lines changed

.github/workflows/tests.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ jobs:
1414
strategy:
1515
max-parallel: 10
1616
matrix:
17-
sql-alchemy: ["1.2", "1.3", "1.4"]
18-
python-version: ["3.7", "3.8", "3.9", "3.10"]
17+
sql-alchemy: [ "1.2", "1.3", "1.4","2.0" ]
18+
python-version: [ "3.7", "3.8", "3.9", "3.10" ]
1919

2020
steps:
2121
- uses: actions/checkout@v3

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ __pycache__/
1212
.Python
1313
env/
1414
.venv/
15+
venv/
1516
build/
1617
develop-eggs/
1718
dist/

graphene_sqlalchemy/batching.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
import sqlalchemy
66
from sqlalchemy.orm import Session, strategies
77
from sqlalchemy.orm.query import QueryContext
8+
from sqlalchemy.util import immutabledict
89

9-
from .utils import SQL_VERSION_HIGHER_EQUAL_THAN_1_4, is_graphene_version_less_than
10+
from .utils import (
11+
SQL_VERSION_HIGHER_EQUAL_THAN_1_4,
12+
SQL_VERSION_HIGHER_EQUAL_THAN_2,
13+
is_graphene_version_less_than,
14+
)
1015

1116

1217
def get_data_loader_impl() -> Any: # pragma: no cover
@@ -76,7 +81,18 @@ async def batch_load_fn(self, parents):
7681
query_context = parent_mapper_query._compile_context()
7782
else:
7883
query_context = QueryContext(session.query(parent_mapper.entity))
79-
if SQL_VERSION_HIGHER_EQUAL_THAN_1_4:
84+
if SQL_VERSION_HIGHER_EQUAL_THAN_2: # pragma: no cover
85+
self.selectin_loader._load_for_path(
86+
query_context,
87+
parent_mapper._path_registry,
88+
states,
89+
None,
90+
child_mapper,
91+
None,
92+
None, # recursion depth can be none
93+
immutabledict(), # default value for selectinload->lazyload
94+
)
95+
elif SQL_VERSION_HIGHER_EQUAL_THAN_1_4:
8096
self.selectin_loader._load_for_path(
8197
query_context,
8298
parent_mapper._path_registry,

graphene_sqlalchemy/tests/models.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@
1616
String,
1717
Table,
1818
func,
19-
select,
2019
)
2120
from sqlalchemy.ext.declarative import declarative_base
2221
from sqlalchemy.ext.hybrid import hybrid_property
2322
from sqlalchemy.orm import backref, column_property, composite, mapper, relationship
24-
from sqlalchemy.sql.sqltypes import _LookupExpressionAdapter
2523
from sqlalchemy.sql.type_api import TypeEngine
2624

25+
from graphene_sqlalchemy.tests.utils import wrap_select_func
26+
from graphene_sqlalchemy.utils import SQL_VERSION_HIGHER_EQUAL_THAN_1_4, SQL_VERSION_HIGHER_EQUAL_THAN_2
27+
28+
# fmt: off
29+
import sqlalchemy
30+
if SQL_VERSION_HIGHER_EQUAL_THAN_2:
31+
from sqlalchemy.sql.sqltypes import HasExpressionLookup # noqa # isort:skip
32+
else:
33+
from sqlalchemy.sql.sqltypes import _LookupExpressionAdapter as HasExpressionLookup # noqa # isort:skip
34+
# fmt: on
35+
2736
PetKind = Enum("cat", "dog", name="pet_kind")
2837

2938

@@ -119,7 +128,7 @@ def hybrid_prop_list(self) -> List[int]:
119128
return [1, 2, 3]
120129

121130
column_prop = column_property(
122-
select([func.cast(func.count(id), Integer)]), doc="Column property"
131+
wrap_select_func(func.cast(func.count(id), Integer)), doc="Column property"
123132
)
124133

125134
composite_prop = composite(
@@ -163,7 +172,11 @@ def __subclasses__(cls):
163172

164173
editor_table = Table("editors", Base.metadata, autoload=True)
165174

166-
mapper(ReflectedEditor, editor_table)
175+
# TODO Remove when switching min sqlalchemy version to SQLAlchemy 1.4
176+
if SQL_VERSION_HIGHER_EQUAL_THAN_1_4:
177+
Base.registry.map_imperatively(ReflectedEditor, editor_table)
178+
else:
179+
mapper(ReflectedEditor, editor_table)
167180

168181

169182
############################################
@@ -337,7 +350,7 @@ class Employee(Person):
337350
############################################
338351

339352

340-
class CustomIntegerColumn(_LookupExpressionAdapter, TypeEngine):
353+
class CustomIntegerColumn(HasExpressionLookup, TypeEngine):
341354
"""
342355
Custom Column Type that our converters don't recognize
343356
Adapted from sqlalchemy.Integer

graphene_sqlalchemy/tests/models_batching.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
String,
1212
Table,
1313
func,
14-
select,
1514
)
1615
from sqlalchemy.ext.declarative import declarative_base
1716
from sqlalchemy.orm import column_property, relationship
1817

18+
from graphene_sqlalchemy.tests.utils import wrap_select_func
19+
1920
PetKind = Enum("cat", "dog", name="pet_kind")
2021

2122

@@ -61,7 +62,7 @@ class Reporter(Base):
6162
favorite_article = relationship("Article", uselist=False)
6263

6364
column_prop = column_property(
64-
select([func.cast(func.count(id), Integer)]), doc="Column property"
65+
wrap_select_func(func.cast(func.count(id), Integer)), doc="Column property"
6566
)
6667

6768

graphene_sqlalchemy/tests/test_converter.py

+32-15
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,28 @@
22
import sys
33
from typing import Dict, Tuple, Union
44

5+
import graphene
56
import pytest
67
import sqlalchemy
78
import sqlalchemy_utils as sqa_utils
8-
from sqlalchemy import Column, func, select, types
9+
from graphene.relay import Node
10+
from graphene.types.structures import Structure
11+
from sqlalchemy import Column, func, types
912
from sqlalchemy.dialects import postgresql
1013
from sqlalchemy.ext.declarative import declarative_base
1114
from sqlalchemy.ext.hybrid import hybrid_property
1215
from sqlalchemy.inspection import inspect
1316
from sqlalchemy.orm import column_property, composite
1417

15-
import graphene
16-
from graphene.relay import Node
17-
from graphene.types.structures import Structure
18-
18+
from .models import (
19+
Article,
20+
CompositeFullName,
21+
Pet,
22+
Reporter,
23+
ShoppingCart,
24+
ShoppingCartItem,
25+
)
26+
from .utils import wrap_select_func
1927
from ..converter import (
2028
convert_sqlalchemy_column,
2129
convert_sqlalchemy_composite,
@@ -27,6 +35,7 @@
2735
from ..fields import UnsortedSQLAlchemyConnectionField, default_connection_field_factory
2836
from ..registry import Registry, get_global_registry
2937
from ..types import ORMField, SQLAlchemyObjectType
38+
from ..utils import is_sqlalchemy_version_less_than
3039
from .models import (
3140
Article,
3241
CompositeFullName,
@@ -204,9 +213,9 @@ def prop_method() -> int | str:
204213
return "not allowed in gql schema"
205214

206215
with pytest.raises(
207-
ValueError,
208-
match=r"Cannot convert hybrid_property Union to "
209-
r"graphene.Union: the Union contains scalars. \.*",
216+
ValueError,
217+
match=r"Cannot convert hybrid_property Union to "
218+
r"graphene.Union: the Union contains scalars. \.*",
210219
):
211220
get_hybrid_property_type(prop_method)
212221

@@ -460,7 +469,7 @@ class TestEnum(enum.IntEnum):
460469

461470
def test_should_columproperty_convert():
462471
field = get_field_from_column(
463-
column_property(select([func.sum(func.cast(id, types.Integer))]).where(id == 1))
472+
column_property(wrap_select_func(func.sum(func.cast(id, types.Integer))).where(id == 1))
464473
)
465474

466475
assert field.type == graphene.Int
@@ -477,10 +486,18 @@ def test_should_jsontype_convert_jsonstring():
477486
assert get_field(types.JSON).type == graphene.JSONString
478487

479488

489+
@pytest.mark.skipif(
490+
(not is_sqlalchemy_version_less_than("2.0.0b1")),
491+
reason="SQLAlchemy >=2.0 does not support this: Variant is no longer used in SQLAlchemy",
492+
)
480493
def test_should_variant_int_convert_int():
481494
assert get_field(types.Variant(types.Integer(), {})).type == graphene.Int
482495

483496

497+
@pytest.mark.skipif(
498+
(not is_sqlalchemy_version_less_than("2.0.0b1")),
499+
reason="SQLAlchemy >=2.0 does not support this: Variant is no longer used in SQLAlchemy",
500+
)
484501
def test_should_variant_string_convert_string():
485502
assert get_field(types.Variant(types.String(), {})).type == graphene.String
486503

@@ -811,8 +828,8 @@ class Meta:
811828
)
812829

813830
for (
814-
hybrid_prop_name,
815-
hybrid_prop_expected_return_type,
831+
hybrid_prop_name,
832+
hybrid_prop_expected_return_type,
816833
) in shopping_cart_item_expected_types.items():
817834
hybrid_prop_field = ShoppingCartItemType._meta.fields[hybrid_prop_name]
818835

@@ -823,7 +840,7 @@ class Meta:
823840
str(hybrid_prop_expected_return_type),
824841
)
825842
assert (
826-
hybrid_prop_field.description is None
843+
hybrid_prop_field.description is None
827844
) # "doc" is ignored by hybrid property
828845

829846
###################################################
@@ -870,8 +887,8 @@ class Meta:
870887
)
871888

872889
for (
873-
hybrid_prop_name,
874-
hybrid_prop_expected_return_type,
890+
hybrid_prop_name,
891+
hybrid_prop_expected_return_type,
875892
) in shopping_cart_expected_types.items():
876893
hybrid_prop_field = ShoppingCartType._meta.fields[hybrid_prop_name]
877894

@@ -882,5 +899,5 @@ class Meta:
882899
str(hybrid_prop_expected_return_type),
883900
)
884901
assert (
885-
hybrid_prop_field.description is None
902+
hybrid_prop_field.description is None
886903
) # "doc" is ignored by hybrid property

graphene_sqlalchemy/tests/utils.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import inspect
22
import re
33

4+
from sqlalchemy import select
5+
6+
from graphene_sqlalchemy.utils import SQL_VERSION_HIGHER_EQUAL_THAN_1_4
7+
48

59
def to_std_dicts(value):
610
"""Convert nested ordered dicts to normal dicts for better comparison."""
@@ -18,8 +22,15 @@ def remove_cache_miss_stat(message):
1822
return re.sub(r"\[generated in \d+.?\d*s\]\s", "", message)
1923

2024

21-
async def eventually_await_session(session, func, *args):
25+
def wrap_select_func(query):
26+
# TODO remove this when we drop support for sqa < 2.0
27+
if SQL_VERSION_HIGHER_EQUAL_THAN_1_4:
28+
return select(query)
29+
else:
30+
return select([query])
31+
2232

33+
async def eventually_await_session(session, func, *args):
2334
if inspect.iscoroutinefunction(getattr(session, func)):
2435
await getattr(session, func)(*args)
2536
else:

graphene_sqlalchemy/utils.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ def is_graphene_version_less_than(version_string): # pragma: no cover
2727

2828
SQL_VERSION_HIGHER_EQUAL_THAN_1_4 = False
2929

30-
if not is_sqlalchemy_version_less_than("1.4"):
30+
if not is_sqlalchemy_version_less_than("1.4"): # pragma: no cover
3131
from sqlalchemy.ext.asyncio import AsyncSession
3232

3333
SQL_VERSION_HIGHER_EQUAL_THAN_1_4 = True
3434

3535

36+
SQL_VERSION_HIGHER_EQUAL_THAN_2 = False
37+
38+
if not is_sqlalchemy_version_less_than("2.0.0b1"): # pragma: no cover
39+
SQL_VERSION_HIGHER_EQUAL_THAN_2 = True
40+
41+
3642
def get_session(context):
3743
return context.get("session")
3844

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# To keep things simple, we only support newer versions of Graphene
1616
"graphene>=3.0.0b7",
1717
"promise>=2.3",
18-
"SQLAlchemy>=1.1,<2",
18+
"SQLAlchemy>=1.1",
1919
"aiodataloader>=0.2.0,<1.0",
2020
]
2121

tox.ini

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = pre-commit,py{37,38,39,310}-sql{12,13,14}
2+
envlist = pre-commit,py{37,38,39,310}-sql{12,13,14,20}
33
skipsdist = true
44
minversion = 3.7.0
55

@@ -15,6 +15,7 @@ SQLALCHEMY =
1515
1.2: sql12
1616
1.3: sql13
1717
1.4: sql14
18+
2.0: sql20
1819

1920
[testenv]
2021
passenv = GITHUB_*
@@ -23,8 +24,11 @@ deps =
2324
sql12: sqlalchemy>=1.2,<1.3
2425
sql13: sqlalchemy>=1.3,<1.4
2526
sql14: sqlalchemy>=1.4,<1.5
27+
sql20: sqlalchemy>=2.0.0b3
28+
setenv =
29+
SQLALCHEMY_WARN_20 = 1
2630
commands =
27-
pytest graphene_sqlalchemy --cov=graphene_sqlalchemy --cov-report=term --cov-report=xml {posargs}
31+
python -W always -m pytest graphene_sqlalchemy --cov=graphene_sqlalchemy --cov-report=term --cov-report=xml {posargs}
2832

2933
[testenv:pre-commit]
3034
basepython=python3.10

0 commit comments

Comments
 (0)