Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to Graphene v3 #306

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,6 @@ target/
# Databases
*.sqlite3
.vscode

# mypy cache
.mypy_cache/
14 changes: 8 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
language: python
matrix:
include:
# Python 2.7
- env: TOXENV=py27
python: 2.7
# Python 3.5
- env: TOXENV=py35
python: 3.5
# Python 3.6
- env: TOXENV=py36
python: 3.6
# Python 3.7
- env: TOXENV=py37
python: 3.7
dist: xenial
# Python 3.8
- env: TOXENV=py38
python: 3.8
dist: xenial
# SQLAlchemy 1.1
- env: TOXENV=py37-sql11
python: 3.7
Expand All @@ -26,6 +24,10 @@ matrix:
- env: TOXENV=py37-sql13
python: 3.7
dist: xenial
# SQLAlchemy 1.4
- env: TOXENV=py37-sql14
python: 3.7
dist: xenial
# Pre-commit
- env: TOXENV=pre-commit
python: 3.7
Expand Down
9 changes: 5 additions & 4 deletions graphene_sqlalchemy/converter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from enum import EnumMeta
from functools import singledispatch

from singledispatch import singledispatch
from sqlalchemy import types
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import interfaces, strategies
Expand Down Expand Up @@ -110,9 +110,9 @@ def _convert_o2m_or_m2m_relationship(relationship_prop, obj_type, batching, conn


def convert_sqlalchemy_hybrid_method(hybrid_prop, resolver, **field_kwargs):
if 'type' not in field_kwargs:
if 'type_' not in field_kwargs:
# TODO The default type should be dependent on the type of the property propety.
field_kwargs['type'] = String
field_kwargs['type_'] = String

return Field(
resolver=resolver,
Expand Down Expand Up @@ -156,7 +156,8 @@ def inner(fn):

def convert_sqlalchemy_column(column_prop, registry, resolver, **field_kwargs):
column = column_prop.columns[0]
field_kwargs.setdefault('type', convert_sqlalchemy_type(getattr(column, "type", None), column, registry))

field_kwargs.setdefault('type_', convert_sqlalchemy_type(getattr(column, "type", None), column, registry))
field_kwargs.setdefault('required', not is_column_nullable(column))
field_kwargs.setdefault('description', get_column_doc(column))

Expand Down
3 changes: 1 addition & 2 deletions graphene_sqlalchemy/enums.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import six
from sqlalchemy.orm import ColumnProperty
from sqlalchemy.types import Enum as SQLAlchemyEnumType

Expand Down Expand Up @@ -63,7 +62,7 @@ def enum_for_field(obj_type, field_name):
if not isinstance(obj_type, type) or not issubclass(obj_type, SQLAlchemyObjectType):
raise TypeError(
"Expected SQLAlchemyObjectType, but got: {!r}".format(obj_type))
if not field_name or not isinstance(field_name, six.string_types):
if not field_name or not isinstance(field_name, str):
raise TypeError(
"Expected a field name, but got: {!r}".format(field_name))
registry = obj_type._meta.registry
Expand Down
67 changes: 41 additions & 26 deletions graphene_sqlalchemy/fields.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
import enum
import warnings
from functools import partial

import six
from promise import Promise, is_thenable
from sqlalchemy.orm.query import Query

from graphene import NonNull
from graphene.relay import Connection, ConnectionField
from graphene.relay.connection import PageInfo
from graphql_relay.connection.arrayconnection import connection_from_list_slice
from graphene.relay.connection import (PageInfo, connection_adapter,
page_info_adapter)
from graphql_relay.connection.arrayconnection import \
connection_from_array_slice

from .batching import get_batch_resolver
from .utils import get_query
from .utils import EnumValue, get_query


class UnsortedSQLAlchemyConnectionField(ConnectionField):
@property
def type(self):
from .types import SQLAlchemyObjectType

_type = super(ConnectionField, self).type
nullable_type = get_nullable_type(_type)
type_ = super(ConnectionField, self).type
nullable_type = get_nullable_type(type_)
if issubclass(nullable_type, Connection):
return _type
return type_
assert issubclass(nullable_type, SQLAlchemyObjectType), (
"SQLALchemyConnectionField only accepts SQLAlchemyObjectType types, not {}"
).format(nullable_type.__name__)
Expand All @@ -31,7 +33,7 @@ def type(self):
), "The type {} doesn't have a connection".format(
nullable_type.__name__
)
assert _type == nullable_type, (
assert type_ == nullable_type, (
"Passing a SQLAlchemyObjectType instance is deprecated. "
"Pass the connection type instead accessible via SQLAlchemyObjectType.connection"
)
Expand All @@ -53,15 +55,19 @@ def resolve_connection(cls, connection_type, model, info, args, resolved):
_len = resolved.count()
else:
_len = len(resolved)
connection = connection_from_list_slice(
resolved,
args,

def adjusted_connection_adapter(edges, pageInfo):
return connection_adapter(connection_type, edges, pageInfo)

connection = connection_from_array_slice(
array_slice=resolved,
args=args,
slice_start=0,
list_length=_len,
list_slice_length=_len,
connection_type=connection_type,
pageinfo_type=PageInfo,
array_length=_len,
array_slice_length=_len,
connection_type=adjusted_connection_adapter,
edge_type=connection_type.Edge,
page_info_type=page_info_adapter,
)
connection.iterable = resolved
connection.length = _len
Expand All @@ -77,7 +83,7 @@ def connection_resolver(cls, resolver, connection_type, model, root, info, **arg

return on_resolve(resolved)

def get_resolver(self, parent_resolver):
def wrap_resolve(self, parent_resolver):
return partial(
self.connection_resolver,
parent_resolver,
Expand All @@ -88,8 +94,8 @@ def get_resolver(self, parent_resolver):

# TODO Rename this to SortableSQLAlchemyConnectionField
class SQLAlchemyConnectionField(UnsortedSQLAlchemyConnectionField):
def __init__(self, type, *args, **kwargs):
nullable_type = get_nullable_type(type)
def __init__(self, type_, *args, **kwargs):
nullable_type = get_nullable_type(type_)
if "sort" not in kwargs and issubclass(nullable_type, Connection):
# Let super class raise if type is not a Connection
try:
Expand All @@ -103,16 +109,25 @@ def __init__(self, type, *args, **kwargs):
)
elif "sort" in kwargs and kwargs["sort"] is None:
del kwargs["sort"]
super(SQLAlchemyConnectionField, self).__init__(type, *args, **kwargs)
super(SQLAlchemyConnectionField, self).__init__(type_, *args, **kwargs)

@classmethod
def get_query(cls, model, info, sort=None, **args):
query = get_query(model, info.context)
if sort is not None:
if isinstance(sort, six.string_types):
query = query.order_by(sort.value)
else:
query = query.order_by(*(col.value for col in sort))
if not isinstance(sort, list):
sort = [sort]
sort_args = []
# ensure consistent handling of graphene Enums, enum values and
# plain strings
for item in sort:
if isinstance(item, enum.Enum):
sort_args.append(item.value.value)
elif isinstance(item, EnumValue):
sort_args.append(item.value)
else:
sort_args.append(item)
query = query.order_by(*sort_args)
return query


Expand All @@ -123,7 +138,7 @@ class BatchSQLAlchemyConnectionField(UnsortedSQLAlchemyConnectionField):
Use at your own risk.
"""

def get_resolver(self, parent_resolver):
def wrap_resolve(self, parent_resolver):
return partial(
self.connection_resolver,
self.resolver,
Expand All @@ -148,13 +163,13 @@ def default_connection_field_factory(relationship, registry, **field_kwargs):
__connectionFactory = UnsortedSQLAlchemyConnectionField


def createConnectionField(_type, **field_kwargs):
def createConnectionField(type_, **field_kwargs):
warnings.warn(
'createConnectionField is deprecated and will be removed in the next '
'major version. Use SQLAlchemyObjectType.Meta.connection_field_factory instead.',
DeprecationWarning,
)
return __connectionFactory(_type, **field_kwargs)
return __connectionFactory(type_, **field_kwargs)


def registerConnectionFieldFactory(factoryMethod):
Expand Down
3 changes: 1 addition & 2 deletions graphene_sqlalchemy/registry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from collections import defaultdict

import six
from sqlalchemy.types import Enum as SQLAlchemyEnumType

from graphene import Enum
Expand Down Expand Up @@ -43,7 +42,7 @@ def register_orm_field(self, obj_type, field_name, orm_field):
raise TypeError(
"Expected SQLAlchemyObjectType, but got: {!r}".format(obj_type)
)
if not field_name or not isinstance(field_name, six.string_types):
if not field_name or not isinstance(field_name, str):
raise TypeError("Expected a field name, but got: {!r}".format(field_name))
self._registry_orm_fields[obj_type][field_name] = orm_field

Expand Down
2 changes: 1 addition & 1 deletion graphene_sqlalchemy/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def convert_composite_class(composite, registry):
return graphene.Field(graphene.Int)


@pytest.yield_fixture(scope="function")
@pytest.fixture(scope="function")
def session_factory():
engine = create_engine(test_db_url)
Base.metadata.create_all(engine)
Expand Down
4 changes: 0 additions & 4 deletions graphene_sqlalchemy/tests/test_benchmark.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import pytest
from graphql.backend import GraphQLCachedBackend, GraphQLCoreBackend

import graphene
from graphene import relay
Expand Down Expand Up @@ -47,15 +46,12 @@ def resolve_reporters(self, info):

def benchmark_query(session_factory, benchmark, query):
schema = get_schema()
cached_backend = GraphQLCachedBackend(GraphQLCoreBackend())
cached_backend.document_from_string(schema, query) # Prime cache

@benchmark
def execute_query():
result = schema.execute(
query,
context_value={"session": session_factory()},
backend=cached_backend,
)
assert not result.errors

Expand Down
3 changes: 2 additions & 1 deletion graphene_sqlalchemy/tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class Model(declarative_base()):
return convert_sqlalchemy_column(column_prop, get_global_registry(), mock_resolver)


def test_should_unknown_sqlalchemy_field_raise_exception():
def _test_should_unknown_sqlalchemy_field_raise_exception():
# TODO: SQLALchemy does not export types.Binary, remove or update this test
re_err = "Don't know how to convert the SQLAlchemy field"
with pytest.raises(Exception, match=re_err):
# support legacy Binary type and subsequent LargeBinary
Expand Down
4 changes: 2 additions & 2 deletions graphene_sqlalchemy/tests/test_query_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def resolve_reporters(self, _info):
def resolve_pets(self, _info, kind):
query = session.query(Pet)
if kind:
query = query.filter_by(pet_kind=kind)
query = query.filter_by(pet_kind=kind.value)
return query

query = """
Expand Down Expand Up @@ -131,7 +131,7 @@ class Query(graphene.ObjectType):
def resolve_pet(self, info, kind=None):
query = session.query(Pet)
if kind:
query = query.filter(Pet.pet_kind == kind)
query = query.filter(Pet.pet_kind == kind.value)
return query.first()

query = """
Expand Down
2 changes: 1 addition & 1 deletion graphene_sqlalchemy/tests/test_sort_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def makeNodes(nodeList):
"""
result = schema.execute(queryError, context_value={"session": session})
assert result.errors is not None
assert '"sort" has invalid value' in result.errors[0].message
assert 'cannot represent non-enum value' in result.errors[0].message

queryNoSort = """
query sortTest {
Expand Down
8 changes: 4 additions & 4 deletions graphene_sqlalchemy/tests/test_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import mock
from unittest import mock

import pytest
import six # noqa F401

from graphene import (Dynamic, Field, GlobalID, Int, List, Node, NonNull,
ObjectType, Schema, String)
Expand Down Expand Up @@ -136,10 +136,10 @@ class Meta:

# columns
email = ORMField(deprecation_reason='Overridden')
email_v2 = ORMField(model_attr='email', type=Int)
email_v2 = ORMField(model_attr='email', type_=Int)

# column_property
column_prop = ORMField(type=String)
column_prop = ORMField(type_=String)

# composite
composite_prop = ORMField()
Expand Down
8 changes: 4 additions & 4 deletions graphene_sqlalchemy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ORMField(OrderedType):
def __init__(
self,
model_attr=None,
type=None,
type_=None,
required=None,
description=None,
deprecation_reason=None,
Expand All @@ -49,7 +49,7 @@ class MyType(SQLAlchemyObjectType):
class Meta:
model = MyModel

id = ORMField(type=graphene.Int)
id = ORMField(type_=graphene.Int)
name = ORMField(required=True)

-> MyType.id will be of type Int (vs ID).
Expand All @@ -58,7 +58,7 @@ class Meta:
:param str model_attr:
Name of the SQLAlchemy model attribute used to resolve this field.
Default to the name of the attribute referencing the ORMField.
:param type:
:param type_:
Default to the type mapping in converter.py.
:param str description:
Default to the `doc` attribute of the SQLAlchemy column property.
Expand All @@ -77,7 +77,7 @@ class Meta:
# The is only useful for documentation and auto-completion
common_kwargs = {
'model_attr': model_attr,
'type': type,
'type_': type_,
'required': required,
'description': description,
'deprecation_reason': deprecation_reason,
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ max-line-length = 120
no_lines_before=FIRSTPARTY
known_graphene=graphene,graphql_relay,flask_graphql,graphql_server,sphinx_graphene_theme
known_first_party=graphene_sqlalchemy
known_third_party=app,database,flask,graphql,mock,models,nameko,pkg_resources,promise,pytest,schema,setuptools,singledispatch,six,sqlalchemy,sqlalchemy_utils
known_third_party=app,database,flask,graphql,models,nameko,pkg_resources,promise,pytest,schema,setuptools,sqlalchemy,sqlalchemy_utils
sections=FUTURE,STDLIB,THIRDPARTY,GRAPHENE,FIRSTPARTY,LOCALFOLDER
skip_glob=examples/nameko_sqlalchemy

Expand Down
Loading