Skip to content

Commit 999b677

Browse files
committed
Road to 2.0
1 parent 2de70b4 commit 999b677

File tree

7 files changed

+98
-75
lines changed

7 files changed

+98
-75
lines changed

graphene_sqlalchemy/converter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List,
77
String)
8-
from graphene.relay import is_node
98
from graphene.types.json import JSONString
109

1110
from .fields import SQLAlchemyConnectionField
@@ -43,7 +42,7 @@ def dynamic_type():
4342
return Field(_type)
4443
elif (direction == interfaces.ONETOMANY or
4544
direction == interfaces.MANYTOMANY):
46-
if is_node(_type):
45+
if _type._meta.connection:
4746
return SQLAlchemyConnectionField(_type)
4847
return Field(List(_type))
4948

graphene_sqlalchemy/fields.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ def model(self):
1919
def get_query(cls, model, context, info, args):
2020
return get_query(model, context)
2121

22+
@property
23+
def type(self):
24+
from .types import SQLAlchemyObjectType
25+
_type = super(ConnectionField, self).type
26+
assert issubclass(_type, SQLAlchemyObjectType), (
27+
"SQLAlchemyConnectionField only accepts SQLAlchemyObjectType types"
28+
)
29+
assert _type._meta.connection, "The type {} doesn't have a connection".format(_type.__name__)
30+
return _type._meta.connection
31+
2232
@classmethod
2333
def connection_resolver(cls, resolver, connection, model, root, args, context, info):
2434
iterable = resolver(root, args, context, info)

graphene_sqlalchemy/tests/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ class Reporter(Base):
3434
articles = relationship('Article', backref='reporter')
3535
favorite_article = relationship("Article", uselist=False)
3636

37+
# total = column_property(
38+
# select([
39+
# func.cast(func.count(PersonInfo.id), Float)
40+
# ])
41+
# )
42+
3743

3844
class Article(Base):
3945
__tablename__ = 'articles'

graphene_sqlalchemy/tests/test_query.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,10 @@ class Input:
277277
ok = graphene.Boolean()
278278
article = graphene.Field(ArticleNode)
279279

280-
@classmethod
281-
def mutate(cls, instance, args, context, info):
280+
def mutate(self, headline, reporter_id):
282281
new_article = Article(
283-
headline=args.get('headline'),
284-
reporter_id=args.get('reporter_id'),
282+
headline=headline,
283+
reporter_id=reporter_id,
285284
)
286285

287286
session.add(new_article)

graphene_sqlalchemy/tests/test_types.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ def test_node_replacedfield():
7171

7272

7373
def test_object_type():
74+
75+
76+
class Human(SQLAlchemyObjectType):
77+
'''Human description'''
78+
79+
pub_date = Int()
80+
81+
class Meta:
82+
model = Article
83+
# exclude_fields = ('id', )
84+
registry = registry
85+
interfaces = (Node, )
86+
7487
assert issubclass(Human, ObjectType)
75-
assert list(Human._meta.fields.keys()) == ['id', 'headline', 'reporter_id', 'reporter', 'pub_date']
88+
assert list(Human._meta.fields.keys()) == ['id', 'headline', 'pub_date', 'reporter_id', 'reporter']
7689
assert is_node(Human)

graphene_sqlalchemy/tests/test_utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from graphene import ObjectType, Schema, String
1+
from graphene import ObjectType, Schema, String, annotate, Context
22

33
from ..utils import get_session
44

@@ -9,7 +9,8 @@ def test_get_session():
99
class Query(ObjectType):
1010
x = String()
1111

12-
def resolve_x(self, args, context, info):
12+
@annotate(context=Context)
13+
def resolve_x(self, context):
1314
return get_session(context)
1415

1516
query = '''

graphene_sqlalchemy/types.py

Lines changed: 61 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
from collections import OrderedDict
22

3-
import six
43
from sqlalchemy.inspection import inspect as sqlalchemyinspect
54
from sqlalchemy.orm.exc import NoResultFound
65

7-
from graphene import Field, ObjectType
8-
from graphene.relay import is_node
9-
from graphene.types.objecttype import ObjectTypeMeta
10-
from graphene.types.options import Options
11-
from graphene.types.utils import merge, yank_fields_from_attrs
12-
from graphene.utils.is_base_type import is_base_type
6+
from graphene import Field # , annotate, ResolveInfo
7+
from graphene.relay import Connection, Node
8+
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
9+
from graphene.types.utils import yank_fields_from_attrs
1310

1411
from .converter import (convert_sqlalchemy_column,
1512
convert_sqlalchemy_composite,
@@ -18,103 +15,102 @@
1815
from .utils import get_query, is_mapped
1916

2017

21-
def construct_fields(options):
22-
only_fields = options.only_fields
23-
exclude_fields = options.exclude_fields
24-
inspected_model = sqlalchemyinspect(options.model)
18+
def construct_fields(model, registry, only_fields, exclude_fields):
19+
inspected_model = sqlalchemyinspect(model)
2520

2621
fields = OrderedDict()
2722

2823
for name, column in inspected_model.columns.items():
2924
is_not_in_only = only_fields and name not in only_fields
30-
is_already_created = name in options.fields
31-
is_excluded = name in exclude_fields or is_already_created
25+
# is_already_created = name in options.fields
26+
is_excluded = name in exclude_fields # or is_already_created
3227
if is_not_in_only or is_excluded:
3328
# We skip this field if we specify only_fields and is not
3429
# in there. Or when we excldue this field in exclude_fields
3530
continue
36-
converted_column = convert_sqlalchemy_column(column, options.registry)
31+
converted_column = convert_sqlalchemy_column(column, registry)
3732
fields[name] = converted_column
3833

3934
for name, composite in inspected_model.composites.items():
4035
is_not_in_only = only_fields and name not in only_fields
41-
is_already_created = name in options.fields
42-
is_excluded = name in exclude_fields or is_already_created
36+
# is_already_created = name in options.fields
37+
is_excluded = name in exclude_fields # or is_already_created
4338
if is_not_in_only or is_excluded:
4439
# We skip this field if we specify only_fields and is not
4540
# in there. Or when we excldue this field in exclude_fields
4641
continue
47-
converted_composite = convert_sqlalchemy_composite(composite, options.registry)
42+
converted_composite = convert_sqlalchemy_composite(composite, registry)
4843
fields[name] = converted_composite
4944

5045
# Get all the columns for the relationships on the model
5146
for relationship in inspected_model.relationships:
5247
is_not_in_only = only_fields and relationship.key not in only_fields
53-
is_already_created = relationship.key in options.fields
54-
is_excluded = relationship.key in exclude_fields or is_already_created
48+
# is_already_created = relationship.key in options.fields
49+
is_excluded = relationship.key in exclude_fields # or is_already_created
5550
if is_not_in_only or is_excluded:
5651
# We skip this field if we specify only_fields and is not
5752
# in there. Or when we excldue this field in exclude_fields
5853
continue
59-
converted_relationship = convert_sqlalchemy_relationship(relationship, options.registry)
54+
converted_relationship = convert_sqlalchemy_relationship(relationship, registry)
6055
name = relationship.key
6156
fields[name] = converted_relationship
6257

6358
return fields
6459

6560

66-
class SQLAlchemyObjectTypeMeta(ObjectTypeMeta):
67-
68-
@staticmethod
69-
def __new__(cls, name, bases, attrs):
70-
# Also ensure initialization is only performed for subclasses of Model
71-
# (excluding Model class itself).
72-
if not is_base_type(bases, SQLAlchemyObjectTypeMeta):
73-
return type.__new__(cls, name, bases, attrs)
74-
75-
options = Options(
76-
attrs.pop('Meta', None),
77-
name=name,
78-
description=attrs.pop('__doc__', None),
79-
model=None,
80-
local_fields=None,
81-
only_fields=(),
82-
exclude_fields=(),
83-
id='id',
84-
interfaces=(),
85-
registry=None
86-
)
61+
class SQLAlchemyObjectTypeOptions(ObjectTypeOptions):
62+
model = None # type: Model
63+
registry = None # type: Registry
64+
connection = None # type: Type[Connection]
65+
id = None # type: str
66+
8767

88-
if not options.registry:
89-
options.registry = get_global_registry()
90-
assert isinstance(options.registry, Registry), (
91-
'The attribute registry in {}.Meta needs to be an'
92-
' instance of Registry, received "{}".'
93-
).format(name, options.registry)
94-
assert is_mapped(options.model), (
68+
class SQLAlchemyObjectType(ObjectType):
69+
@classmethod
70+
def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False,
71+
only_fields=(), exclude_fields=(), connection=None,
72+
use_connection=None, interfaces=(), id=None, **options):
73+
assert is_mapped(model), (
9574
'You need to pass a valid SQLAlchemy Model in '
9675
'{}.Meta, received "{}".'
97-
).format(name, options.model)
76+
).format(cls.__name__, model)
9877

99-
cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options))
78+
if not registry:
79+
registry = get_global_registry()
10080

101-
options.registry.register(cls)
81+
assert isinstance(registry, Registry), (
82+
'The attribute registry in {} needs to be an instance of '
83+
'Registry, received "{}".'
84+
).format(cls.__name__, registry)
10285

103-
options.sqlalchemy_fields = yank_fields_from_attrs(
104-
construct_fields(options),
86+
sqla_fields = yank_fields_from_attrs(
87+
construct_fields(model, registry, only_fields, exclude_fields),
10588
_as=Field,
10689
)
107-
options.fields = merge(
108-
options.interface_fields,
109-
options.sqlalchemy_fields,
110-
options.base_fields,
111-
options.local_fields
112-
)
11390

114-
return cls
91+
if use_connection is None and interfaces:
92+
use_connection = any((issubclass(interface, Node) for interface in interfaces))
93+
94+
if use_connection and not connection:
95+
# We create the connection automatically
96+
connection = Connection.create_type('{}Connection'.format(cls.__name__), node=cls)
97+
98+
if connection is not None:
99+
assert issubclass(connection, Connection), (
100+
"The connection must be a Connection. Received {}"
101+
).format(connection.__name__)
102+
103+
_meta = SQLAlchemyObjectTypeOptions(cls)
104+
_meta.model = model
105+
_meta.registry = registry
106+
_meta.fields = sqla_fields
107+
_meta.connection = connection
108+
_meta.id = id or 'id'
115109

110+
super(SQLAlchemyObjectType, cls).__init_subclass_with_meta__(_meta=_meta, interfaces=interfaces, **options)
116111

117-
class SQLAlchemyObjectType(six.with_metaclass(SQLAlchemyObjectTypeMeta, ObjectType)):
112+
if not skip_registry:
113+
registry.register(cls)
118114

119115
@classmethod
120116
def is_type_of(cls, root, context, info):
@@ -138,8 +134,7 @@ def get_node(cls, id, context, info):
138134
except NoResultFound:
139135
return None
140136

141-
def resolve_id(self, args, context, info):
142-
graphene_type = info.parent_type.graphene_type
143-
if is_node(graphene_type):
144-
return self.__mapper__.primary_key_from_instance(self)[0]
145-
return getattr(self, graphene_type._meta.id, None)
137+
# @annotate(info=ResolveInfo)
138+
def resolve_id(self):
139+
# graphene_type = info.parent_type.graphene_type
140+
return self.__mapper__.primary_key_from_instance(self)[0]

0 commit comments

Comments
 (0)