From 565241555f3dddda53b5dea882f69e5d39c0189d Mon Sep 17 00:00:00 2001 From: Jonathan Ehwald Date: Mon, 12 Oct 2020 00:12:17 +0200 Subject: [PATCH 01/32] Add graphene >=3.0b5 to the dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e20a1750..2a3cfd60 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ requirements = [ # To keep things simple, we only support newer versions of Graphene - "graphene>=2.1.3,<3", + "graphene>=3.0b5", "promise>=2.3", # Tests fail with 1.0.19 "SQLAlchemy>=1.2,<2", From 9a16b0cf8e5f382f0165997d56f969e39b23419d Mon Sep 17 00:00:00 2001 From: Jonathan Ehwald Date: Mon, 12 Oct 2020 00:13:12 +0200 Subject: [PATCH 02/32] Remove backend usage from the benchmark tests --- graphene_sqlalchemy/tests/test_benchmark.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_benchmark.py b/graphene_sqlalchemy/tests/test_benchmark.py index 1e5ee4f1..8d8a111f 100644 --- a/graphene_sqlalchemy/tests/test_benchmark.py +++ b/graphene_sqlalchemy/tests/test_benchmark.py @@ -1,6 +1,4 @@ import pytest -from graphql.backend import GraphQLCachedBackend, GraphQLCoreBackend - import graphene from graphene import relay @@ -47,15 +45,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 From 106843f72a7cbf8fa05b1ed3ff8a75fadb7e1f85 Mon Sep 17 00:00:00 2001 From: Jonathan Ehwald Date: Tue, 5 Jan 2021 18:38:25 +0100 Subject: [PATCH 03/32] Make the running of single tests using tox possible --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 69d84f92..46c6bbef 100644 --- a/tox.ini +++ b/tox.ini @@ -37,4 +37,4 @@ commands = basepython = python3.9 deps = -e.[dev] commands = - flake8 --exclude setup.py,docs,examples,tests,.tox --max-line-length 120 \ No newline at end of file + flake8 --exclude setup.py,docs,examples,tests,.tox --max-line-length 120 From f1937f5dbac7c9d192861805c212e30831be83f0 Mon Sep 17 00:00:00 2001 From: Jonathan Ehwald Date: Tue, 5 Jan 2021 18:42:02 +0100 Subject: [PATCH 04/32] Resolve shadowing of the type keyword The same has been done in graphene. --- graphene_sqlalchemy/converter.py | 7 ++++--- graphene_sqlalchemy/fields.py | 18 +++++++++--------- graphene_sqlalchemy/tests/test_types.py | 4 ++-- graphene_sqlalchemy/types.py | 8 ++++---- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index f4b805e2..b5629770 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -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, @@ -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)) diff --git a/graphene_sqlalchemy/fields.py b/graphene_sqlalchemy/fields.py index 780fcbf0..2fad07ef 100644 --- a/graphene_sqlalchemy/fields.py +++ b/graphene_sqlalchemy/fields.py @@ -19,10 +19,10 @@ class UnsortedSQLAlchemyConnectionField(ConnectionField): 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__) @@ -31,7 +31,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" ) @@ -88,8 +88,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: @@ -103,7 +103,7 @@ 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): @@ -148,13 +148,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): diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index bf563b6e..eeaafe3b 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -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() diff --git a/graphene_sqlalchemy/types.py b/graphene_sqlalchemy/types.py index ff22cded..72f06c06 100644 --- a/graphene_sqlalchemy/types.py +++ b/graphene_sqlalchemy/types.py @@ -27,7 +27,7 @@ class ORMField(OrderedType): def __init__( self, model_attr=None, - type=None, + type_=None, required=None, description=None, deprecation_reason=None, @@ -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). @@ -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. @@ -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, From 09673a8a0bce6fff93e8ad08f41dc96691ba0356 Mon Sep 17 00:00:00 2001 From: Jonathan Ehwald Date: Tue, 5 Jan 2021 18:47:41 +0100 Subject: [PATCH 05/32] Port enum related tests --- graphene_sqlalchemy/tests/test_query_enums.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_query_enums.py b/graphene_sqlalchemy/tests/test_query_enums.py index ec585d57..5166c45f 100644 --- a/graphene_sqlalchemy/tests/test_query_enums.py +++ b/graphene_sqlalchemy/tests/test_query_enums.py @@ -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 = """ @@ -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 = """ From 3458214d4a19aa8b40fc99903d117be46df3e992 Mon Sep 17 00:00:00 2001 From: Jonathan Ehwald Date: Tue, 5 Jan 2021 18:48:55 +0100 Subject: [PATCH 06/32] Port relay connection fields --- graphene_sqlalchemy/fields.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/graphene_sqlalchemy/fields.py b/graphene_sqlalchemy/fields.py index 2fad07ef..cb0fccf9 100644 --- a/graphene_sqlalchemy/fields.py +++ b/graphene_sqlalchemy/fields.py @@ -7,8 +7,8 @@ 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 @@ -53,15 +53,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 @@ -77,7 +81,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, @@ -123,7 +127,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, From 4879b4b90bcf86442ea4b44f4b20ce3db7ad5f0a Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Sun, 2 May 2021 10:22:23 +0200 Subject: [PATCH 07/32] Remove dependency on external mock package. Python 3 bundles mock in stdlib. --- graphene_sqlalchemy/tests/test_types.py | 3 ++- setup.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index eeaafe3b..ffa0333d 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -1,4 +1,5 @@ -import mock +from unittest import mock + import pytest import six # noqa F401 diff --git a/setup.py b/setup.py index 2a3cfd60..2c5ef30f 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,6 @@ tests_require = [ "pytest==4.3.1", - "mock==2.0.0", "pytest-cov==2.6.1", "sqlalchemy_utils==0.33.9", "pytest-benchmark==3.2.1", From c9ef0a9b7a7a2aefe4f48fff0bc66fd10a56d224 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Sun, 2 May 2021 10:52:49 +0200 Subject: [PATCH 08/32] Pin SQLAlchemy below 1.4 until we figure out what needs upgrading. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2c5ef30f..ae61e706 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ "graphene>=3.0b5", "promise>=2.3", # Tests fail with 1.0.19 - "SQLAlchemy>=1.2,<2", + "SQLAlchemy>=1.2,<1.4", "six>=1.10.0,<2", "singledispatch>=3.4.0.3,<4", ] From 0f9ea7fa300e902e2d7a5920939d46d81db150e4 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Sun, 2 May 2021 11:50:54 +0200 Subject: [PATCH 09/32] Ignore mypy cache. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a97b8c21..c4a735fe 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,6 @@ target/ # Databases *.sqlite3 .vscode + +# mypy cache +.mypy_cache/ From f73dd3dfd24f16b53fccabad94c6ff477b18342a Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Mon, 3 May 2021 20:00:05 +0200 Subject: [PATCH 10/32] Use sqlalchemy_utils which supports SQLAlchemy 1.4 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ae61e706..26993ceb 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ "graphene>=3.0b5", "promise>=2.3", # Tests fail with 1.0.19 - "SQLAlchemy>=1.2,<1.4", + "SQLAlchemy>=1.2,<2.0", "six>=1.10.0,<2", "singledispatch>=3.4.0.3,<4", ] @@ -28,7 +28,7 @@ tests_require = [ "pytest==4.3.1", "pytest-cov==2.6.1", - "sqlalchemy_utils==0.33.9", + "sqlalchemy_utils==0.37.0", "pytest-benchmark==3.2.1", ] From 510ea1e8d012748cff53165d8c18c364f5e8ce21 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Mon, 3 May 2021 20:03:26 +0200 Subject: [PATCH 11/32] Upgrade test dependencies. Allow for some flexibility on version numbers. --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 26993ceb..6b7e7818 100644 --- a/setup.py +++ b/setup.py @@ -26,10 +26,10 @@ requirements.append("enum34 >= 1.1.6") tests_require = [ - "pytest==4.3.1", - "pytest-cov==2.6.1", - "sqlalchemy_utils==0.37.0", - "pytest-benchmark==3.2.1", + "pytest>=6.2.0,<7.0", + "pytest-cov>=2.11.0,<3.0", + "sqlalchemy_utils>=0.37.0,<1.0", + "pytest-benchmark>=3.4.0,<4.0", ] setup( From 92cee005b2016dafb583bbec1f9d07c458a0c24e Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Tue, 4 May 2021 12:15:12 +0200 Subject: [PATCH 12/32] Use built-in singledispatch. --- graphene_sqlalchemy/converter.py | 2 +- setup.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index b5629770..f782e5d0 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -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 diff --git a/setup.py b/setup.py index 6b7e7818..c30b4fd8 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ # Tests fail with 1.0.19 "SQLAlchemy>=1.2,<2.0", "six>=1.10.0,<2", - "singledispatch>=3.4.0.3,<4", ] try: import enum From beca5d71046838b72dbd562f40c083a3b775ae8f Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Tue, 4 May 2021 12:16:00 +0200 Subject: [PATCH 13/32] Remove dependency on six. --- graphene_sqlalchemy/enums.py | 3 +-- graphene_sqlalchemy/fields.py | 5 ++--- graphene_sqlalchemy/registry.py | 3 +-- graphene_sqlalchemy/tests/test_types.py | 1 - setup.py | 1 - 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/graphene_sqlalchemy/enums.py b/graphene_sqlalchemy/enums.py index 0adea107..f100be19 100644 --- a/graphene_sqlalchemy/enums.py +++ b/graphene_sqlalchemy/enums.py @@ -1,4 +1,3 @@ -import six from sqlalchemy.orm import ColumnProperty from sqlalchemy.types import Enum as SQLAlchemyEnumType @@ -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 diff --git a/graphene_sqlalchemy/fields.py b/graphene_sqlalchemy/fields.py index cb0fccf9..abfe0c52 100644 --- a/graphene_sqlalchemy/fields.py +++ b/graphene_sqlalchemy/fields.py @@ -1,7 +1,6 @@ import warnings from functools import partial -import six from promise import Promise, is_thenable from sqlalchemy.orm.query import Query @@ -56,7 +55,7 @@ def resolve_connection(cls, connection_type, model, info, args, resolved): def adjusted_connection_adapter(edges, pageInfo): return connection_adapter(connection_type, edges, pageInfo) - + connection = connection_from_array_slice( array_slice=resolved, args=args, @@ -113,7 +112,7 @@ def __init__(self, type_, *args, **kwargs): 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): + if isinstance(sort, str): query = query.order_by(sort.value) else: query = query.order_by(*(col.value for col in sort)) diff --git a/graphene_sqlalchemy/registry.py b/graphene_sqlalchemy/registry.py index c20bc2ca..acfa744b 100644 --- a/graphene_sqlalchemy/registry.py +++ b/graphene_sqlalchemy/registry.py @@ -1,6 +1,5 @@ from collections import defaultdict -import six from sqlalchemy.types import Enum as SQLAlchemyEnumType from graphene import Enum @@ -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 diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index ffa0333d..32f01509 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -1,7 +1,6 @@ from unittest import mock import pytest -import six # noqa F401 from graphene import (Dynamic, Field, GlobalID, Int, List, Node, NonNull, ObjectType, Schema, String) diff --git a/setup.py b/setup.py index c30b4fd8..ecfb0922 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ "promise>=2.3", # Tests fail with 1.0.19 "SQLAlchemy>=1.2,<2.0", - "six>=1.10.0,<2", ] try: import enum From acca38c2e826ff0555d4cd2fc444fed503359d44 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Tue, 4 May 2021 12:16:32 +0200 Subject: [PATCH 14/32] Assume enum is always available. As we're targeting Python >= 3.6, this module should always be in stdlib. --- setup.cfg | 2 +- setup.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4e8e5029..4e554938 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/setup.py b/setup.py index ecfb0922..64b69848 100644 --- a/setup.py +++ b/setup.py @@ -18,10 +18,6 @@ # Tests fail with 1.0.19 "SQLAlchemy>=1.2,<2.0", ] -try: - import enum -except ImportError: # Python < 2.7 and Python 3.3 - requirements.append("enum34 >= 1.1.6") tests_require = [ "pytest>=6.2.0,<7.0", From db84788148f99db2f946556a735a8df1e29d7c73 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Tue, 4 May 2021 12:31:30 +0200 Subject: [PATCH 15/32] Add more tox factors for Python 3.8 and SQLAlchemy 1.4 --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 46c6bbef..129b93a9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pre-commit,py{27,35,36,37,38,39}-sql{12,13},flake8 +envlist = pre-commit,py{36,37,38}-sql{11,12,13,14} skipsdist = true minversion = 3.7.0 @@ -23,6 +23,7 @@ deps = .[test] sql12: sqlalchemy>=1.2,<1.3 sql13: sqlalchemy>=1.3,<1.4 + sql14: sqlalchemy>=1.4,<1.5 commands = pytest graphene_sqlalchemy --cov=graphene_sqlalchemy --cov-report=term --cov-report=xml {posargs} From 85d7e989716556e8f2b559d53531fcdc046f5746 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Tue, 4 May 2021 12:36:53 +0200 Subject: [PATCH 16/32] Use non-deprecated fixture decorator for session_factory. --- graphene_sqlalchemy/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/tests/conftest.py b/graphene_sqlalchemy/tests/conftest.py index 98515051..34ba9d8a 100644 --- a/graphene_sqlalchemy/tests/conftest.py +++ b/graphene_sqlalchemy/tests/conftest.py @@ -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) From 6f0508e675c9a0f923aa0cc201f2663679418676 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Tue, 4 May 2021 15:06:53 +0200 Subject: [PATCH 17/32] Ensure consistent handling of graphene Enums, enum values and plain strings. --- graphene_sqlalchemy/fields.py | 26 ++++++++++++++------ graphene_sqlalchemy/tests/test_benchmark.py | 1 + graphene_sqlalchemy/tests/test_sort_enums.py | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/graphene_sqlalchemy/fields.py b/graphene_sqlalchemy/fields.py index abfe0c52..f756277a 100644 --- a/graphene_sqlalchemy/fields.py +++ b/graphene_sqlalchemy/fields.py @@ -1,3 +1,4 @@ +import enum import warnings from functools import partial @@ -6,11 +7,13 @@ from graphene import NonNull from graphene.relay import Connection, ConnectionField -from graphene.relay.connection import PageInfo, connection_adapter, page_info_adapter -from graphql_relay.connection.arrayconnection import connection_from_array_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): @@ -112,10 +115,19 @@ def __init__(self, type_, *args, **kwargs): def get_query(cls, model, info, sort=None, **args): query = get_query(model, info.context) if sort is not None: - if isinstance(sort, str): - 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 diff --git a/graphene_sqlalchemy/tests/test_benchmark.py b/graphene_sqlalchemy/tests/test_benchmark.py index 8d8a111f..d2d78592 100644 --- a/graphene_sqlalchemy/tests/test_benchmark.py +++ b/graphene_sqlalchemy/tests/test_benchmark.py @@ -1,4 +1,5 @@ import pytest + import graphene from graphene import relay diff --git a/graphene_sqlalchemy/tests/test_sort_enums.py b/graphene_sqlalchemy/tests/test_sort_enums.py index d6f6965d..6291d4f8 100644 --- a/graphene_sqlalchemy/tests/test_sort_enums.py +++ b/graphene_sqlalchemy/tests/test_sort_enums.py @@ -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 { From 81c4c8658409f31fd1f7ef03bcb8643992b822dd Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Tue, 4 May 2021 15:17:00 +0200 Subject: [PATCH 18/32] Disable converter test for Binary type. SQLAlchemy 1.4+ does not export Binary directly. We should decide whether to keep this test, update for another unknown type, or remove it altogether. --- graphene_sqlalchemy/tests/test_converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index f0fc1802..5f362d5d 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -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 From 52c718d21f2c202573ced4aaa3d977d89d82eec4 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Tue, 4 May 2021 15:34:18 +0200 Subject: [PATCH 19/32] Keep consistent versions between travis.yml and setup.py --- .travis.yml | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 6 ++---- 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..18ef6ca8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,49 @@ +language: python +matrix: + include: + # 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 + dist: xenial + # SQLAlchemy 1.2 + - env: TOXENV=py37-sql12 + python: 3.7 + dist: xenial + # SQLAlchemy 1.3 + - 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 + dist: xenial +install: pip install .[dev] +script: tox +after_success: coveralls +cache: + directories: + - $HOME/.cache/pip + - $HOME/.cache/pre-commit +deploy: + provider: pypi + user: syrusakbary + on: + tags: true + password: + secure: q0ey31cWljGB30l43aEd1KIPuAHRutzmsd2lBb/2zvD79ReBrzvCdFAkH2xcyo4Volk3aazQQTNUIurnTuvBxmtqja0e+gUaO5LdOcokVdOGyLABXh7qhd2kdvbTDWgSwA4EWneLGXn/SjXSe0f3pCcrwc6WDcLAHxtffMvO9gulpYQtUoOqXfMipMOkRD9iDWTJBsSo3trL70X1FHOVr6Yqi0mfkX2Y/imxn6wlTWRz28Ru94xrj27OmUnCv7qcG0taO8LNlUCquNFAr2sZ+l+U/GkQrrM1y+ehPz3pmI0cCCd7SX/7+EG9ViZ07BZ31nk4pgnqjmj3nFwqnCE/4IApGnduqtrMDF63C9TnB1TU8oJmbbUCu4ODwRpBPZMnwzaHsLnrpdrB89/98NtTfujdrh3U5bVB+t33yxrXVh+FjgLYj9PVeDixpFDn6V/Xcnv4BbRMNOhXIQT7a7/5b99RiXBjCk6KRu+Jdu5DZ+3G4Nbr4oim3kZFPUHa555qbzTlwAfkrQxKv3C3OdVJR7eGc9ADsbHyEJbdPNAh/T+xblXTXLS3hPYDvgM+WEGy3CytBDG3JVcXm25ZP96EDWjweJ7MyfylubhuKj/iR1Y1wiHeIsYq9CqRrFQUWL8gFJBfmgjs96xRXXXnvyLtKUKpKw3wFg5cR/6FnLeYZ8k= + distributions: "sdist bdist_wheel" diff --git a/setup.py b/setup.py index 64b69848..e18b56ad 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ "graphene>=3.0b5", "promise>=2.3", # Tests fail with 1.0.19 - "SQLAlchemy>=1.2,<2.0", + "SQLAlchemy>=1.1,<2.0", ] tests_require = [ @@ -39,12 +39,10 @@ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: PyPy", ], keywords="api graphql protocol rest relay graphene", From ee8a28b7f9e66afde53e735c750a5c10761708c5 Mon Sep 17 00:00:00 2001 From: Ricardo Madriz Date: Fri, 10 Sep 2021 08:57:11 -0600 Subject: [PATCH 20/32] Change if condition ChoiceType no longer relies on EnumMeta but instead it exposes a ``type_impl`` field that we can leverage to decide whether the internal ``choices`` is a tuple of enum members or a tuple of tuples (pairs) --- graphene_sqlalchemy/converter.py | 8 ++++++-- setup.cfg | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index f782e5d0..1720e3d8 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,4 +1,3 @@ -from enum import EnumMeta from functools import singledispatch from sqlalchemy import types @@ -21,6 +20,11 @@ except ImportError: ChoiceType = JSONType = ScalarListType = TSVectorType = object +try: + from sqlalchemy_utils.types.choice import EnumTypeImpl +except ImportError: + EnumTypeImpl = object + is_selectin_available = getattr(strategies, 'SelectInLoader', None) @@ -222,7 +226,7 @@ def convert_enum_to_enum(type, column, registry=None): @convert_sqlalchemy_type.register(ChoiceType) def convert_choice_to_enum(type, column, registry=None): name = "{}_{}".format(column.table.name, column.name).upper() - if isinstance(type.choices, EnumMeta): + if isinstance(type.type_impl, EnumTypeImpl): # type.choices may be Enum/IntEnum, in ChoiceType both presented as EnumMeta # do not use from_enum here because we can have more than one enum column in table return Enum(name, list((v.name, v.value) for v in type.choices)) diff --git a/setup.cfg b/setup.cfg index 4e554938..600e59b9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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,models,nameko,pkg_resources,promise,pytest,schema,setuptools,sqlalchemy,sqlalchemy_utils +known_third_party=app,database,flask,models,nameko,pkg_resources,promise,pytest,schema,setuptools,sqlalchemy,sqlalchemy_utils sections=FUTURE,STDLIB,THIRDPARTY,GRAPHENE,FIRSTPARTY,LOCALFOLDER skip_glob=examples/nameko_sqlalchemy From ed3cd1a26d080a3f5d7b570c26e1cb7aee1a2b5d Mon Sep 17 00:00:00 2001 From: Ricardo Madriz Date: Fri, 10 Sep 2021 10:29:39 -0600 Subject: [PATCH 21/32] Bump version --- graphene_sqlalchemy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/__init__.py b/graphene_sqlalchemy/__init__.py index 3945d506..060bd13b 100644 --- a/graphene_sqlalchemy/__init__.py +++ b/graphene_sqlalchemy/__init__.py @@ -2,7 +2,7 @@ from .fields import SQLAlchemyConnectionField from .utils import get_query, get_session -__version__ = "2.3.0" +__version__ = "3.0.0b1" __all__ = [ "__version__", From 69c4fa1c4b13aef52fbe4e61041c61bae530e89b Mon Sep 17 00:00:00 2001 From: Ricardo Madriz Date: Fri, 10 Sep 2021 13:59:36 -0600 Subject: [PATCH 22/32] Replace promises.dataloader with aiodataloader --- graphene_sqlalchemy/batching.py | 12 ++++++------ setup.cfg | 2 +- setup.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/graphene_sqlalchemy/batching.py b/graphene_sqlalchemy/batching.py index baf01deb..b80c3a51 100644 --- a/graphene_sqlalchemy/batching.py +++ b/graphene_sqlalchemy/batching.py @@ -1,5 +1,5 @@ +import aiodataloader import sqlalchemy -from promise import dataloader, promise from sqlalchemy.orm import Session, strategies from sqlalchemy.orm.query import QueryContext @@ -10,10 +10,10 @@ def get_batch_resolver(relationship_prop): # This is so SQL string generation is cached under-the-hood via `bakery` selectin_loader = strategies.SelectInLoader(relationship_prop, (('lazy', 'selectin'),)) - class RelationshipLoader(dataloader.DataLoader): + class RelationshipLoader(aiodataloader.DataLoader): cache = False - def batch_load_fn(self, parents): # pylint: disable=method-hidden + async def batch_load_fn(self, parents): """ Batch loads the relationships of all the parents as one SQL statement. @@ -62,11 +62,11 @@ def batch_load_fn(self, parents): # pylint: disable=method-hidden child_mapper, ) - return promise.Promise.resolve([getattr(parent, relationship_prop.key) for parent in parents]) + return [getattr(parent, relationship_prop.key) for parent in parents] loader = RelationshipLoader() - def resolve(root, info, **args): - return loader.load(root) + async def resolve(root, info, **args): + return await loader.load(root) return resolve diff --git a/setup.cfg b/setup.cfg index 600e59b9..f36334d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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,models,nameko,pkg_resources,promise,pytest,schema,setuptools,sqlalchemy,sqlalchemy_utils +known_third_party=aiodataloader,app,database,flask,models,nameko,pkg_resources,promise,pytest,schema,setuptools,sqlalchemy,sqlalchemy_utils sections=FUTURE,STDLIB,THIRDPARTY,GRAPHENE,FIRSTPARTY,LOCALFOLDER skip_glob=examples/nameko_sqlalchemy diff --git a/setup.py b/setup.py index e18b56ad..d7958db7 100644 --- a/setup.py +++ b/setup.py @@ -13,10 +13,10 @@ requirements = [ # To keep things simple, we only support newer versions of Graphene - "graphene>=3.0b5", + "graphene>=3.0.0b7", "promise>=2.3", - # Tests fail with 1.0.19 "SQLAlchemy>=1.1,<2.0", + "aiodataloader>=0.2.0,<1.0", ] tests_require = [ From 30fc2df39625e600dfd212454cfdb9809052783e Mon Sep 17 00:00:00 2001 From: Ricardo Madriz Date: Fri, 10 Sep 2021 15:11:32 -0600 Subject: [PATCH 23/32] Fix batching tests --- graphene_sqlalchemy/tests/test_batching.py | 25 +++++++++++++--------- setup.py | 3 ++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_batching.py b/graphene_sqlalchemy/tests/test_batching.py index fc646a3c..9879bb49 100644 --- a/graphene_sqlalchemy/tests/test_batching.py +++ b/graphene_sqlalchemy/tests/test_batching.py @@ -75,7 +75,8 @@ def resolve_reporters(self, info): pytest.skip('SQL batching only works for SQLAlchemy 1.2+', allow_module_level=True) -def test_many_to_one(session_factory): +@pytest.mark.asyncio +async def test_many_to_one(session_factory): session = session_factory() reporter_1 = Reporter( @@ -103,7 +104,7 @@ def test_many_to_one(session_factory): with mock_sqlalchemy_logging_handler() as sqlalchemy_logging_handler: # Starts new session to fully reset the engine / connection logging level session = session_factory() - result = schema.execute(""" + result = await schema.execute_async(""" query { articles { headline @@ -166,7 +167,8 @@ def test_many_to_one(session_factory): } -def test_one_to_one(session_factory): +@pytest.mark.asyncio +async def test_one_to_one(session_factory): session = session_factory() reporter_1 = Reporter( @@ -194,7 +196,7 @@ def test_one_to_one(session_factory): with mock_sqlalchemy_logging_handler() as sqlalchemy_logging_handler: # Starts new session to fully reset the engine / connection logging level session = session_factory() - result = schema.execute(""" + result = await schema.execute_async(""" query { reporters { firstName @@ -257,7 +259,8 @@ def test_one_to_one(session_factory): } -def test_one_to_many(session_factory): +@pytest.mark.asyncio +async def test_one_to_many(session_factory): session = session_factory() reporter_1 = Reporter( @@ -293,7 +296,7 @@ def test_one_to_many(session_factory): with mock_sqlalchemy_logging_handler() as sqlalchemy_logging_handler: # Starts new session to fully reset the engine / connection logging level session = session_factory() - result = schema.execute(""" + result = await schema.execute_async(""" query { reporters { firstName @@ -382,7 +385,8 @@ def test_one_to_many(session_factory): } -def test_many_to_many(session_factory): +@pytest.mark.asyncio +async def test_many_to_many(session_factory): session = session_factory() reporter_1 = Reporter( @@ -420,7 +424,7 @@ def test_many_to_many(session_factory): with mock_sqlalchemy_logging_handler() as sqlalchemy_logging_handler: # Starts new session to fully reset the engine / connection logging level session = session_factory() - result = schema.execute(""" + result = await schema.execute_async(""" query { reporters { firstName @@ -586,7 +590,8 @@ def resolve_reporters(self, info): assert len(select_statements) == 2 -def test_connection_factory_field_overrides_batching_is_false(session_factory): +@pytest.mark.asyncio +async def test_connection_factory_field_overrides_batching_is_false(session_factory): session = session_factory() reporter_1 = Reporter(first_name='Reporter_1') session.add(reporter_1) @@ -620,7 +625,7 @@ def resolve_reporters(self, info): with mock_sqlalchemy_logging_handler() as sqlalchemy_logging_handler: # Starts new session to fully reset the engine / connection logging level session = session_factory() - schema.execute(""" + await schema.execute_async(""" query { reporters { articles { diff --git a/setup.py b/setup.py index d7958db7..0e433030 100644 --- a/setup.py +++ b/setup.py @@ -15,12 +15,13 @@ # To keep things simple, we only support newer versions of Graphene "graphene>=3.0.0b7", "promise>=2.3", - "SQLAlchemy>=1.1,<2.0", + "SQLAlchemy>=1.1,<1.4", "aiodataloader>=0.2.0,<1.0", ] tests_require = [ "pytest>=6.2.0,<7.0", + "pytest-asyncio>=0.15.1", "pytest-cov>=2.11.0,<3.0", "sqlalchemy_utils>=0.37.0,<1.0", "pytest-benchmark>=3.4.0,<4.0", From e6042ad2ff9914fe4db9fd4648935d5931152b56 Mon Sep 17 00:00:00 2001 From: Ricardo Madriz Date: Fri, 10 Sep 2021 16:02:01 -0600 Subject: [PATCH 24/32] Remove batch from schema due to pytest-benchmark lack of support --- graphene_sqlalchemy/tests/test_benchmark.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_benchmark.py b/graphene_sqlalchemy/tests/test_benchmark.py index d2d78592..82f6e0cd 100644 --- a/graphene_sqlalchemy/tests/test_benchmark.py +++ b/graphene_sqlalchemy/tests/test_benchmark.py @@ -3,7 +3,6 @@ import graphene from graphene import relay -from ..fields import BatchSQLAlchemyConnectionField from ..types import SQLAlchemyObjectType from .models import Article, HairKind, Pet, Reporter from .utils import is_sqlalchemy_version_less_than @@ -17,19 +16,16 @@ class ReporterType(SQLAlchemyObjectType): class Meta: model = Reporter interfaces = (relay.Node,) - connection_field_factory = BatchSQLAlchemyConnectionField.from_relationship class ArticleType(SQLAlchemyObjectType): class Meta: model = Article interfaces = (relay.Node,) - connection_field_factory = BatchSQLAlchemyConnectionField.from_relationship class PetType(SQLAlchemyObjectType): class Meta: model = Pet interfaces = (relay.Node,) - connection_field_factory = BatchSQLAlchemyConnectionField.from_relationship class Query(graphene.ObjectType): articles = graphene.Field(graphene.List(ArticleType)) From 3ec8b171d901eb871f1bd3f5350a9e4387bcd5d4 Mon Sep 17 00:00:00 2001 From: Ricardo Madriz Date: Fri, 10 Sep 2021 17:02:52 -0600 Subject: [PATCH 25/32] Remove unused import --- graphene_sqlalchemy/fields.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/fields.py b/graphene_sqlalchemy/fields.py index f756277a..a22a3ae7 100644 --- a/graphene_sqlalchemy/fields.py +++ b/graphene_sqlalchemy/fields.py @@ -7,8 +7,7 @@ from graphene import NonNull from graphene.relay import Connection, ConnectionField -from graphene.relay.connection import (PageInfo, connection_adapter, - page_info_adapter) +from graphene.relay.connection import connection_adapter, page_info_adapter from graphql_relay.connection.arrayconnection import \ connection_from_array_slice From 2eb712f9c850b4182ff5c6d47e71f10d6e01735f Mon Sep 17 00:00:00 2001 From: Ricardo Madriz Date: Fri, 17 Sep 2021 12:28:35 -0600 Subject: [PATCH 26/32] Delete .travis.yml --- .travis.yml | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 18ef6ca8..00000000 --- a/.travis.yml +++ /dev/null @@ -1,49 +0,0 @@ -language: python -matrix: - include: - # 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 - dist: xenial - # SQLAlchemy 1.2 - - env: TOXENV=py37-sql12 - python: 3.7 - dist: xenial - # SQLAlchemy 1.3 - - 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 - dist: xenial -install: pip install .[dev] -script: tox -after_success: coveralls -cache: - directories: - - $HOME/.cache/pip - - $HOME/.cache/pre-commit -deploy: - provider: pypi - user: syrusakbary - on: - tags: true - password: - secure: q0ey31cWljGB30l43aEd1KIPuAHRutzmsd2lBb/2zvD79ReBrzvCdFAkH2xcyo4Volk3aazQQTNUIurnTuvBxmtqja0e+gUaO5LdOcokVdOGyLABXh7qhd2kdvbTDWgSwA4EWneLGXn/SjXSe0f3pCcrwc6WDcLAHxtffMvO9gulpYQtUoOqXfMipMOkRD9iDWTJBsSo3trL70X1FHOVr6Yqi0mfkX2Y/imxn6wlTWRz28Ru94xrj27OmUnCv7qcG0taO8LNlUCquNFAr2sZ+l+U/GkQrrM1y+ehPz3pmI0cCCd7SX/7+EG9ViZ07BZ31nk4pgnqjmj3nFwqnCE/4IApGnduqtrMDF63C9TnB1TU8oJmbbUCu4ODwRpBPZMnwzaHsLnrpdrB89/98NtTfujdrh3U5bVB+t33yxrXVh+FjgLYj9PVeDixpFDn6V/Xcnv4BbRMNOhXIQT7a7/5b99RiXBjCk6KRu+Jdu5DZ+3G4Nbr4oim3kZFPUHa555qbzTlwAfkrQxKv3C3OdVJR7eGc9ADsbHyEJbdPNAh/T+xblXTXLS3hPYDvgM+WEGy3CytBDG3JVcXm25ZP96EDWjweJ7MyfylubhuKj/iR1Y1wiHeIsYq9CqRrFQUWL8gFJBfmgjs96xRXXXnvyLtKUKpKw3wFg5cR/6FnLeYZ8k= - distributions: "sdist bdist_wheel" From 63888d3a014adb63cdddac3a21d7be95d2ab6634 Mon Sep 17 00:00:00 2001 From: Cole Lin Date: Sat, 11 Sep 2021 16:15:20 +0800 Subject: [PATCH 27/32] Allow sqlalchemy 1.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0e433030..da49f1d4 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ # To keep things simple, we only support newer versions of Graphene "graphene>=3.0.0b7", "promise>=2.3", - "SQLAlchemy>=1.1,<1.4", + "SQLAlchemy>=1.1,<2", "aiodataloader>=0.2.0,<1.0", ] From 340b29d52fd404a462337f5ac802355aa6f46a78 Mon Sep 17 00:00:00 2001 From: Cole Lin Date: Sat, 11 Sep 2021 17:07:22 +0800 Subject: [PATCH 28/32] Fix syntax error in Batching from signature changes --- graphene_sqlalchemy/batching.py | 35 +++++++++++++++------ graphene_sqlalchemy/tests/test_batching.py | 3 +- graphene_sqlalchemy/tests/test_benchmark.py | 2 +- graphene_sqlalchemy/tests/test_converter.py | 2 +- graphene_sqlalchemy/tests/utils.py | 8 ----- graphene_sqlalchemy/utils.py | 6 ++++ 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/graphene_sqlalchemy/batching.py b/graphene_sqlalchemy/batching.py index b80c3a51..85cc8855 100644 --- a/graphene_sqlalchemy/batching.py +++ b/graphene_sqlalchemy/batching.py @@ -3,6 +3,8 @@ from sqlalchemy.orm import Session, strategies from sqlalchemy.orm.query import QueryContext +from .utils import is_sqlalchemy_version_less_than + def get_batch_resolver(relationship_prop): @@ -52,15 +54,30 @@ async def batch_load_fn(self, parents): states = [(sqlalchemy.inspect(parent), True) for parent in parents] # For our purposes, the query_context will only used to get the session - query_context = QueryContext(session.query(parent_mapper.entity)) - - selectin_loader._load_for_path( - query_context, - parent_mapper._path_registry, - states, - None, - child_mapper, - ) + query_context = None + if is_sqlalchemy_version_less_than('1.4'): + query_context = QueryContext(session.query(parent_mapper.entity)) + else: + parent_mapper_query = session.query(parent_mapper.entity) + query_context = parent_mapper_query._compile_context() + + if is_sqlalchemy_version_less_than('1.4'): + selectin_loader._load_for_path( + query_context, + parent_mapper._path_registry, + states, + None, + child_mapper + ) + else: + selectin_loader._load_for_path( + query_context, + parent_mapper._path_registry, + states, + None, + child_mapper, + None + ) return [getattr(parent, relationship_prop.key) for parent in parents] diff --git a/graphene_sqlalchemy/tests/test_batching.py b/graphene_sqlalchemy/tests/test_batching.py index 9879bb49..0770a26e 100644 --- a/graphene_sqlalchemy/tests/test_batching.py +++ b/graphene_sqlalchemy/tests/test_batching.py @@ -10,7 +10,8 @@ default_connection_field_factory) from ..types import ORMField, SQLAlchemyObjectType from .models import Article, HairKind, Pet, Reporter -from .utils import is_sqlalchemy_version_less_than, to_std_dicts +from ..utils import is_sqlalchemy_version_less_than +from .utils import to_std_dicts class MockLoggingHandler(logging.Handler): diff --git a/graphene_sqlalchemy/tests/test_benchmark.py b/graphene_sqlalchemy/tests/test_benchmark.py index 82f6e0cd..11e9d0e0 100644 --- a/graphene_sqlalchemy/tests/test_benchmark.py +++ b/graphene_sqlalchemy/tests/test_benchmark.py @@ -4,8 +4,8 @@ from graphene import relay from ..types import SQLAlchemyObjectType +from ..utils import is_sqlalchemy_version_less_than from .models import Article, HairKind, Pet, Reporter -from .utils import is_sqlalchemy_version_less_than if is_sqlalchemy_version_less_than('1.2'): pytest.skip('SQL batching only works for SQLAlchemy 1.2+', allow_module_level=True) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index 5f362d5d..dee6ec8f 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -47,7 +47,7 @@ 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): diff --git a/graphene_sqlalchemy/tests/utils.py b/graphene_sqlalchemy/tests/utils.py index 428757c3..b59ab0e8 100644 --- a/graphene_sqlalchemy/tests/utils.py +++ b/graphene_sqlalchemy/tests/utils.py @@ -1,6 +1,3 @@ -import pkg_resources - - def to_std_dicts(value): """Convert nested ordered dicts to normal dicts for better comparison.""" if isinstance(value, dict): @@ -9,8 +6,3 @@ def to_std_dicts(value): return [to_std_dicts(v) for v in value] else: return value - - -def is_sqlalchemy_version_less_than(version_string): - """Check the installed SQLAlchemy version""" - return pkg_resources.get_distribution('SQLAlchemy').parsed_version < pkg_resources.parse_version(version_string) diff --git a/graphene_sqlalchemy/utils.py b/graphene_sqlalchemy/utils.py index 7139eefc..b30c0eb4 100644 --- a/graphene_sqlalchemy/utils.py +++ b/graphene_sqlalchemy/utils.py @@ -1,6 +1,7 @@ import re import warnings +import pkg_resources from sqlalchemy.exc import ArgumentError from sqlalchemy.orm import class_mapper, object_mapper from sqlalchemy.orm.exc import UnmappedClassError, UnmappedInstanceError @@ -140,3 +141,8 @@ def sort_argument_for_model(cls, has_default=True): enum.default = None return Argument(List(enum), default_value=enum.default) + + +def is_sqlalchemy_version_less_than(version_string): + """Check the installed SQLAlchemy version""" + return pkg_resources.get_distribution('SQLAlchemy').parsed_version < pkg_resources.parse_version(version_string) From ecbc00ffba61e6e72d3712f83b4b291d51db1c72 Mon Sep 17 00:00:00 2001 From: Cole Lin Date: Sat, 11 Sep 2021 18:08:26 +0800 Subject: [PATCH 29/32] Fix types.binary cannot be imported --- graphene_sqlalchemy/tests/test_converter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index dee6ec8f..3196d003 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -48,11 +48,10 @@ class Model(declarative_base()): 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 - get_field(getattr(types, 'LargeBinary', types.Binary)()) + get_field(getattr(types, 'LargeBinary', types.BINARY)()) def test_should_date_convert_string(): From b2d2dd9ff70a18501fd71468ba647b7383f1a979 Mon Sep 17 00:00:00 2001 From: Cole Lin Date: Wed, 15 Sep 2021 23:18:25 +0800 Subject: [PATCH 30/32] Fix test cases for batching in sqlalchemy1.4 --- graphene_sqlalchemy/tests/test_batching.py | 114 +++++---------------- graphene_sqlalchemy/tests/utils.py | 9 ++ 2 files changed, 36 insertions(+), 87 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_batching.py b/graphene_sqlalchemy/tests/test_batching.py index 0770a26e..1896900b 100644 --- a/graphene_sqlalchemy/tests/test_batching.py +++ b/graphene_sqlalchemy/tests/test_batching.py @@ -1,3 +1,4 @@ +import ast import contextlib import logging @@ -9,9 +10,9 @@ from ..fields import (BatchSQLAlchemyConnectionField, default_connection_field_factory) from ..types import ORMField, SQLAlchemyObjectType -from .models import Article, HairKind, Pet, Reporter from ..utils import is_sqlalchemy_version_less_than -from .utils import to_std_dicts +from .models import Article, HairKind, Pet, Reporter +from .utils import remove_cache_miss_stat, to_std_dicts class MockLoggingHandler(logging.Handler): @@ -127,26 +128,12 @@ async def test_many_to_one(session_factory): assert len(sql_statements) == 1 return - assert messages == [ - 'BEGIN (implicit)', - - 'SELECT articles.id AS articles_id, ' - 'articles.headline AS articles_headline, ' - 'articles.pub_date AS articles_pub_date, ' - 'articles.reporter_id AS articles_reporter_id \n' - 'FROM articles', - '()', - - 'SELECT reporters.id AS reporters_id, ' - '(SELECT CAST(count(reporters.id) AS INTEGER) AS anon_2 \nFROM reporters) AS anon_1, ' - 'reporters.first_name AS reporters_first_name, ' - 'reporters.last_name AS reporters_last_name, ' - 'reporters.email AS reporters_email, ' - 'reporters.favorite_pet_kind AS reporters_favorite_pet_kind \n' - 'FROM reporters \n' - 'WHERE reporters.id IN (?, ?)', - '(1, 2)', - ] + if not is_sqlalchemy_version_less_than('1.4'): + messages[2] = remove_cache_miss_stat(messages[2]) + messages[4] = remove_cache_miss_stat(messages[4]) + + assert ast.literal_eval(messages[2]) == () + assert sorted(ast.literal_eval(messages[4])) == [1, 2] assert not result.errors result = to_std_dicts(result.data) @@ -219,26 +206,12 @@ async def test_one_to_one(session_factory): assert len(sql_statements) == 1 return - assert messages == [ - 'BEGIN (implicit)', - - 'SELECT (SELECT CAST(count(reporters.id) AS INTEGER) AS anon_2 \nFROM reporters) AS anon_1, ' - 'reporters.id AS reporters_id, ' - 'reporters.first_name AS reporters_first_name, ' - 'reporters.last_name AS reporters_last_name, ' - 'reporters.email AS reporters_email, ' - 'reporters.favorite_pet_kind AS reporters_favorite_pet_kind \n' - 'FROM reporters', - '()', - - 'SELECT articles.reporter_id AS articles_reporter_id, ' - 'articles.id AS articles_id, ' - 'articles.headline AS articles_headline, ' - 'articles.pub_date AS articles_pub_date \n' - 'FROM articles \n' - 'WHERE articles.reporter_id IN (?, ?)', - '(1, 2)' - ] + if not is_sqlalchemy_version_less_than('1.4'): + messages[2] = remove_cache_miss_stat(messages[2]) + messages[4] = remove_cache_miss_stat(messages[4]) + + assert ast.literal_eval(messages[2]) == () + assert sorted(ast.literal_eval(messages[4])) == [1, 2] assert not result.errors result = to_std_dicts(result.data) @@ -323,26 +296,12 @@ async def test_one_to_many(session_factory): assert len(sql_statements) == 1 return - assert messages == [ - 'BEGIN (implicit)', - - 'SELECT (SELECT CAST(count(reporters.id) AS INTEGER) AS anon_2 \nFROM reporters) AS anon_1, ' - 'reporters.id AS reporters_id, ' - 'reporters.first_name AS reporters_first_name, ' - 'reporters.last_name AS reporters_last_name, ' - 'reporters.email AS reporters_email, ' - 'reporters.favorite_pet_kind AS reporters_favorite_pet_kind \n' - 'FROM reporters', - '()', - - 'SELECT articles.reporter_id AS articles_reporter_id, ' - 'articles.id AS articles_id, ' - 'articles.headline AS articles_headline, ' - 'articles.pub_date AS articles_pub_date \n' - 'FROM articles \n' - 'WHERE articles.reporter_id IN (?, ?)', - '(1, 2)' - ] + if not is_sqlalchemy_version_less_than('1.4'): + messages[2] = remove_cache_miss_stat(messages[2]) + messages[4] = remove_cache_miss_stat(messages[4]) + + assert ast.literal_eval(messages[2]) == () + assert sorted(ast.literal_eval(messages[4])) == [1, 2] assert not result.errors result = to_std_dicts(result.data) @@ -451,31 +410,12 @@ async def test_many_to_many(session_factory): assert len(sql_statements) == 1 return - assert messages == [ - 'BEGIN (implicit)', - - 'SELECT (SELECT CAST(count(reporters.id) AS INTEGER) AS anon_2 \nFROM reporters) AS anon_1, ' - 'reporters.id AS reporters_id, ' - 'reporters.first_name AS reporters_first_name, ' - 'reporters.last_name AS reporters_last_name, ' - 'reporters.email AS reporters_email, ' - 'reporters.favorite_pet_kind AS reporters_favorite_pet_kind \n' - 'FROM reporters', - '()', - - 'SELECT reporters_1.id AS reporters_1_id, ' - 'pets.id AS pets_id, ' - 'pets.name AS pets_name, ' - 'pets.pet_kind AS pets_pet_kind, ' - 'pets.hair_kind AS pets_hair_kind, ' - 'pets.reporter_id AS pets_reporter_id \n' - 'FROM reporters AS reporters_1 ' - 'JOIN association AS association_1 ON reporters_1.id = association_1.reporter_id ' - 'JOIN pets ON pets.id = association_1.pet_id \n' - 'WHERE reporters_1.id IN (?, ?) ' - 'ORDER BY pets.id', - '(1, 2)' - ] + if not is_sqlalchemy_version_less_than('1.4'): + messages[2] = remove_cache_miss_stat(messages[2]) + messages[4] = remove_cache_miss_stat(messages[4]) + + assert ast.literal_eval(messages[2]) == () + assert sorted(ast.literal_eval(messages[4])) == [1, 2] assert not result.errors result = to_std_dicts(result.data) diff --git a/graphene_sqlalchemy/tests/utils.py b/graphene_sqlalchemy/tests/utils.py index b59ab0e8..c90ee476 100644 --- a/graphene_sqlalchemy/tests/utils.py +++ b/graphene_sqlalchemy/tests/utils.py @@ -1,3 +1,6 @@ +import re + + def to_std_dicts(value): """Convert nested ordered dicts to normal dicts for better comparison.""" if isinstance(value, dict): @@ -6,3 +9,9 @@ def to_std_dicts(value): return [to_std_dicts(v) for v in value] else: return value + + +def remove_cache_miss_stat(message): + """Remove the stat from the echoed query message when the cache is missed for sqlalchemy version >= 1.4""" + # https://github.com/sqlalchemy/sqlalchemy/blob/990eb3d8813369d3b8a7776ae85fb33627443d30/lib/sqlalchemy/engine/default.py#L1177 + return re.sub(r"\[generated in \d+.?\d*s\]\s", "", message) From 4cbc93b62d75d6b79a3381674d1cab9ac1a68cab Mon Sep 17 00:00:00 2001 From: Cole Lin Date: Sat, 18 Sep 2021 12:07:46 +0800 Subject: [PATCH 31/32] Add tests for sqlalchemy1.4 --- .github/workflows/tests.yml | 50 ++++++++++++++++++------------------- tox.ini | 1 + 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4adb26f6..6c47b94b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,31 +8,31 @@ jobs: strategy: max-parallel: 10 matrix: - sql-alchemy: ["1.2", "1.3"] + sql-alchemy: ["1.2", "1.3", "1.4"] python-version: ["3.6", "3.7", "3.8", "3.9"] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox - env: - SQLALCHEMY: ${{ matrix.sql-alchemy }} - TOXENV: ${{ matrix.toxenv }} - - name: Upload coverage.xml - if: ${{ matrix.sql-alchemy == '1.3' && matrix.python-version == '3.9' }} - uses: actions/upload-artifact@v2 - with: - name: graphene-sqlalchemy-coverage - path: coverage.xml - if-no-files-found: error - - name: Upload coverage.xml to codecov - if: ${{ matrix.sql-alchemy == '1.3' && matrix.python-version == '3.9' }} - uses: codecov/codecov-action@v1 \ No newline at end of file + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox + env: + SQLALCHEMY: ${{ matrix.sql-alchemy }} + TOXENV: ${{ matrix.toxenv }} + - name: Upload coverage.xml + if: ${{ matrix.sql-alchemy == '1.3' && matrix.python-version == '3.9' }} + uses: actions/upload-artifact@v2 + with: + name: graphene-sqlalchemy-coverage + path: coverage.xml + if-no-files-found: error + - name: Upload coverage.xml to codecov + if: ${{ matrix.sql-alchemy == '1.3' && matrix.python-version == '3.9' }} + uses: codecov/codecov-action@v1 diff --git a/tox.ini b/tox.ini index 129b93a9..643e1122 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ python = SQLALCHEMY = 1.2: sql12 1.3: sql13 + 1.4: sql14 [testenv] passenv = GITHUB_* From 8829d01111b496d884c8b751780ee5d2e2b99449 Mon Sep 17 00:00:00 2001 From: Cole Lin Date: Sat, 18 Sep 2021 13:23:24 +0800 Subject: [PATCH 32/32] Upload coverage when sqlalchemy is 1.4 --- .github/workflows/tests.yml | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6c47b94b..a9a3bd5d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,12 +27,12 @@ jobs: SQLALCHEMY: ${{ matrix.sql-alchemy }} TOXENV: ${{ matrix.toxenv }} - name: Upload coverage.xml - if: ${{ matrix.sql-alchemy == '1.3' && matrix.python-version == '3.9' }} + if: ${{ matrix.sql-alchemy == '1.4' && matrix.python-version == '3.9' }} uses: actions/upload-artifact@v2 with: name: graphene-sqlalchemy-coverage path: coverage.xml if-no-files-found: error - name: Upload coverage.xml to codecov - if: ${{ matrix.sql-alchemy == '1.3' && matrix.python-version == '3.9' }} + if: ${{ matrix.sql-alchemy == '1.4' && matrix.python-version == '3.9' }} uses: codecov/codecov-action@v1 diff --git a/tox.ini b/tox.ini index 643e1122..a2843f05 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pre-commit,py{36,37,38}-sql{11,12,13,14} +envlist = pre-commit,py{36,37,38,39}-sql{11,12,13,14} skipsdist = true minversion = 3.7.0