Skip to content

Commit 18dd02a

Browse files
geertjanvdkGeert Vanderkelen
authored and
Geert Vanderkelen
committed
Allow creation of custom connections. Fixes #65.
Previously, it was not possible to create custom connections without monkey patching graphene.relay.connection. Custom connections are useful when all connections need a total count for paging, for example. We change the SQLAlchemyObjectType.__init_subclass_with_meta method so that when the connection argument is a class, it is used to create the new connection type. A test case has been added to check usage of custom connections. The `conftest.py` was added to allow pytest using database related fixtures in other places than `test_query.py`.
1 parent 4827ce2 commit 18dd02a

File tree

4 files changed

+142
-98
lines changed

4 files changed

+142
-98
lines changed

graphene_sqlalchemy/tests/conftest.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import pytest
2+
from sqlalchemy import create_engine
3+
from sqlalchemy.orm import scoped_session, sessionmaker
4+
5+
from .models import Article, Base, Editor, Reporter
6+
from ..registry import reset_global_registry
7+
8+
db = create_engine('sqlite:///test_sqlalchemy.sqlite3')
9+
10+
11+
@pytest.yield_fixture(scope='function')
12+
def session():
13+
reset_global_registry()
14+
connection = db.engine.connect()
15+
transaction = connection.begin()
16+
Base.metadata.create_all(connection)
17+
18+
# options = dict(bind=connection, binds={})
19+
session_factory = sessionmaker(bind=connection)
20+
session = scoped_session(session_factory)
21+
22+
yield session
23+
24+
# Finalize test here
25+
transaction.rollback()
26+
connection.close()
27+
session.remove()
28+
29+
@pytest.yield_fixture(scope='function')
30+
def setup_fixtures(session):
31+
reporter = Reporter(first_name='ABA', last_name='X')
32+
session.add(reporter)
33+
reporter2 = Reporter(first_name='ABO', last_name='Y')
34+
session.add(reporter2)
35+
article = Article(headline='Hi!')
36+
article.reporter = reporter
37+
session.add(article)
38+
editor = Editor(name="John")
39+
session.add(editor)
40+
session.commit()

graphene_sqlalchemy/tests/test_query.py

Lines changed: 5 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,12 @@
1-
import pytest
2-
from sqlalchemy import create_engine
3-
from sqlalchemy.orm import scoped_session, sessionmaker
4-
51
import graphene
62
from graphene.relay import Node
73

8-
from ..registry import reset_global_registry
4+
from .models import Article, Editor, Reporter
95
from ..fields import SQLAlchemyConnectionField
106
from ..types import SQLAlchemyObjectType
11-
from .models import Article, Base, Editor, Pet, Reporter
12-
13-
db = create_engine('sqlite:///test_sqlalchemy.sqlite3')
14-
15-
16-
@pytest.yield_fixture(scope='function')
17-
def session():
18-
reset_global_registry()
19-
connection = db.engine.connect()
20-
transaction = connection.begin()
21-
Base.metadata.create_all(connection)
22-
23-
# options = dict(bind=connection, binds={})
24-
session_factory = sessionmaker(bind=connection)
25-
session = scoped_session(session_factory)
26-
27-
yield session
28-
29-
# Finalize test here
30-
transaction.rollback()
31-
connection.close()
32-
session.remove()
337

348

35-
def setup_fixtures(session):
36-
pet = Pet(name='Lassie', pet_kind='dog')
37-
session.add(pet)
38-
reporter = Reporter(first_name='ABA', last_name='X')
39-
session.add(reporter)
40-
reporter2 = Reporter(first_name='ABO', last_name='Y')
41-
session.add(reporter2)
42-
article = Article(headline='Hi!')
43-
article.reporter = reporter
44-
session.add(article)
45-
editor = Editor(name="John")
46-
session.add(editor)
47-
session.commit()
48-
49-
50-
def test_should_query_well(session):
51-
setup_fixtures(session)
9+
def test_should_query_well(session, setup_fixtures):
5210

5311
class ReporterType(SQLAlchemyObjectType):
5412

@@ -95,42 +53,7 @@ def resolve_reporters(self, *args, **kwargs):
9553
assert result.data == expected
9654

9755

98-
def test_should_query_enums(session):
99-
setup_fixtures(session)
100-
101-
class PetType(SQLAlchemyObjectType):
102-
103-
class Meta:
104-
model = Pet
105-
106-
class Query(graphene.ObjectType):
107-
pet = graphene.Field(PetType)
108-
109-
def resolve_pet(self, *args, **kwargs):
110-
return session.query(Pet).first()
111-
112-
query = '''
113-
query PetQuery {
114-
pet {
115-
name,
116-
petKind
117-
}
118-
}
119-
'''
120-
expected = {
121-
'pet': {
122-
'name': 'Lassie',
123-
'petKind': 'dog'
124-
}
125-
}
126-
schema = graphene.Schema(query=Query)
127-
result = schema.execute(query)
128-
assert not result.errors
129-
assert result.data == expected, result.data
130-
131-
132-
def test_should_node(session):
133-
setup_fixtures(session)
56+
def test_should_node(session, setup_fixtures):
13457

13558
class ReporterNode(SQLAlchemyObjectType):
13659

@@ -229,8 +152,7 @@ def resolve_article(self, *args, **kwargs):
229152
assert result.data == expected
230153

231154

232-
def test_should_custom_identifier(session):
233-
setup_fixtures(session)
155+
def test_should_custom_identifier(session, setup_fixtures):
234156

235157
class EditorNode(SQLAlchemyObjectType):
236158

@@ -279,8 +201,7 @@ class Query(graphene.ObjectType):
279201
assert result.data == expected
280202

281203

282-
def test_should_mutate_well(session):
283-
setup_fixtures(session)
204+
def test_should_mutate_well(session, setup_fixtures):
284205

285206
class EditorNode(SQLAlchemyObjectType):
286207

graphene_sqlalchemy/tests/test_types.py

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
2-
from graphene import Field, Int, Interface, ObjectType
1+
from graphene import Field, Int, Interface, ObjectType, Connection, Schema
32
from graphene.relay import Node, is_node
4-
import six
3+
import pytest
54

65
from ..registry import Registry
76
from ..types import SQLAlchemyObjectType
7+
from ..fields import SQLAlchemyConnectionField
88
from .models import Article, Reporter
99

1010
registry = Registry()
@@ -72,8 +72,6 @@ def test_node_replacedfield():
7272

7373

7474
def test_object_type():
75-
76-
7775
class Human(SQLAlchemyObjectType):
7876
'''Human description'''
7977

@@ -90,7 +88,6 @@ class Meta:
9088
assert is_node(Human)
9189

9290

93-
9491
# Test Custom SQLAlchemyObjectType Implementation
9592
class CustomSQLAlchemyObjectType(SQLAlchemyObjectType):
9693
class Meta:
@@ -116,3 +113,77 @@ def test_custom_objecttype_registered():
116113
'pets',
117114
'articles',
118115
'favorite_article']
116+
117+
118+
def test_custom_connection(session, setup_fixtures):
119+
exp_counter = 123
120+
121+
class CustomConnection(Connection):
122+
class Meta:
123+
abstract = True
124+
125+
counter = Int()
126+
127+
@staticmethod
128+
def resolve_counter(*args, **kwargs):
129+
return exp_counter
130+
131+
class ArticleType(SQLAlchemyObjectType):
132+
class Meta:
133+
model = Article
134+
connection = CustomConnection
135+
interfaces = (Node,)
136+
registry = registry
137+
138+
class Query(ObjectType):
139+
articles = SQLAlchemyConnectionField(ArticleType)
140+
141+
schema = Schema(query=Query)
142+
result = schema.execute("query { articles { counter edges { node { headline }}}}",
143+
context_value={'session': session})
144+
145+
assert not result.errors
146+
assert result.data['articles']['counter'] == exp_counter
147+
assert result.data['articles']['edges'][0]['node']['headline'] == 'Hi!'
148+
149+
150+
def test_automatically_created_connection():
151+
expected = "ArticleTypeConnection"
152+
153+
class ArticleType(SQLAlchemyObjectType):
154+
class Meta:
155+
model = Article
156+
interfaces = (Node,)
157+
registry = registry
158+
159+
assert ArticleType._meta.connection.__name__ == expected
160+
161+
162+
def test_passing_connection_instance():
163+
expected = "CnxHumanType"
164+
165+
class HumanType(SQLAlchemyObjectType):
166+
class Meta:
167+
model = Reporter
168+
interfaces = (Node,)
169+
170+
class ArticleType(SQLAlchemyObjectType):
171+
class Meta:
172+
model = Article
173+
interfaces = (Node,)
174+
connection = Connection.create_type(expected, node=HumanType)
175+
registry = registry
176+
177+
assert ArticleType._meta.connection.__name__ == expected
178+
179+
180+
def test_passing_incorrect_connection_instance():
181+
with pytest.raises(AssertionError) as excinfo:
182+
class ArticleType(SQLAlchemyObjectType):
183+
class Meta:
184+
model = Article
185+
interfaces = (Node,)
186+
connection = 'spam'
187+
registry = registry
188+
189+
assert "The connection must be a Connection. Received" in str(excinfo.value)

graphene_sqlalchemy/types.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections import OrderedDict
2+
from inspect import isclass
23

34
from sqlalchemy.inspection import inspect as sqlalchemyinspect
45
from sqlalchemy.ext.hybrid import hybrid_property
@@ -112,20 +113,31 @@ def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=Fa
112113
if use_connection is None and interfaces:
113114
use_connection = any((issubclass(interface, Node) for interface in interfaces))
114115

115-
if use_connection and not connection:
116-
# We create the connection automatically
117-
connection = Connection.create_type('{}Connection'.format(cls.__name__), node=cls)
118-
119-
if connection is not None:
120-
assert issubclass(connection, Connection), (
116+
cnx = None
117+
if use_connection:
118+
if connection and isclass(connection) and issubclass(connection, Connection) and \
119+
not hasattr(connection, '_meta'):
120+
# Create connection type automatically using given class
121+
cnx = connection.create_type('{}Connection'.format(cls.__name__), node=cls)
122+
elif not connection:
123+
# Create connection type automatically using graphene.relay.Connection
124+
cnx = Connection.create_type('{}Connection'.format(cls.__name__), node=cls)
125+
else:
126+
cnx = connection
127+
128+
if cnx is not None:
129+
assert isclass(cnx), (
130+
"The connection must be a Connection. Received {}"
131+
).format(type(cnx))
132+
assert issubclass(cnx, Connection), (
121133
"The connection must be a Connection. Received {}"
122-
).format(connection.__name__)
134+
).format(cnx.__name__)
123135

124136
_meta = SQLAlchemyObjectTypeOptions(cls)
125137
_meta.model = model
126138
_meta.registry = registry
127139
_meta.fields = sqla_fields
128-
_meta.connection = connection
140+
_meta.connection = cnx
129141
_meta.id = id or 'id'
130142

131143
super(SQLAlchemyObjectType, cls).__init_subclass_with_meta__(_meta=_meta, interfaces=interfaces, **options)

0 commit comments

Comments
 (0)