Skip to content

Commit 8205df7

Browse files
committed
🥅 Don't suppress SQLAlchemy errors when mapping classes
These changes modify graphene-sqlalchemy to not suppress errors coming from SQLAlchemy when attempting to map classes. Previously this made the debugging experience difficult, since issues with SQLAlchemy models would produce an unclear error message from graphene-sqlalchemy. With these changes, the SQLAlchemy error is propagated to the end-user, making it possible for them to easily correct the true issue.
1 parent 869a55b commit 8205df7

File tree

4 files changed

+81
-8
lines changed

4 files changed

+81
-8
lines changed

Diff for: ‎.pre-commit-config.yaml

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
default_language_version:
22
python: python3.7
33
repos:
4-
- repo: git://github.com/pre-commit/pre-commit-hooks
4+
- repo: https://github.com/pre-commit/pre-commit-hooks
55
rev: c8bad492e1b1d65d9126dba3fe3bd49a5a52b9d6 # v2.1.0
66
hooks:
77
- id: check-merge-conflict
@@ -11,15 +11,15 @@ repos:
1111
exclude: ^docs/.*$
1212
- id: trailing-whitespace
1313
exclude: README.md
14-
- repo: git://github.com/PyCQA/flake8
14+
- repo: https://github.com/PyCQA/flake8
1515
rev: 88caf5ac484f5c09aedc02167c59c66ff0af0068 # 3.7.7
1616
hooks:
1717
- id: flake8
18-
- repo: git://github.com/asottile/seed-isort-config
18+
- repo: https://github.com/asottile/seed-isort-config
1919
rev: v1.7.0
2020
hooks:
2121
- id: seed-isort-config
22-
- repo: git://github.com/pre-commit/mirrors-isort
22+
- repo: https://github.com/pre-commit/mirrors-isort
2323
rev: v4.3.4
2424
hooks:
2525
- id: isort

Diff for: ‎graphene_sqlalchemy/tests/test_types.py

+65
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from unittest import mock
22

33
import pytest
4+
import sqlalchemy.exc
5+
import sqlalchemy.orm.exc
46

57
from graphene import (Dynamic, Field, GlobalID, Int, List, Node, NonNull,
68
ObjectType, Schema, String)
79
from graphene.relay import Connection
810

11+
from .. import utils
912
from ..converter import convert_sqlalchemy_composite
1013
from ..fields import (SQLAlchemyConnectionField,
1114
UnsortedSQLAlchemyConnectionField, createConnectionField,
@@ -492,3 +495,65 @@ class Meta:
492495
def test_deprecated_createConnectionField():
493496
with pytest.warns(DeprecationWarning):
494497
createConnectionField(None)
498+
499+
500+
@mock.patch(utils.__name__ + '.class_mapper')
501+
def test_unique_errors_propagate(class_mapper_mock):
502+
# Define unique error to detect
503+
class UniqueError(Exception):
504+
pass
505+
506+
# Mock class_mapper effect
507+
class_mapper_mock.side_effect = UniqueError
508+
509+
# Make sure that errors are propagated from class_mapper when instantiating new classes
510+
error = None
511+
try:
512+
class ArticleOne(SQLAlchemyObjectType):
513+
class Meta(object):
514+
model = Article
515+
except UniqueError as e:
516+
error = e
517+
518+
# Check that an error occured, and that it was the unique error we gave
519+
assert error is not None
520+
assert isinstance(error, UniqueError)
521+
522+
523+
@mock.patch(utils.__name__ + '.class_mapper')
524+
def test_argument_errors_propagate(class_mapper_mock):
525+
# Mock class_mapper effect
526+
class_mapper_mock.side_effect = sqlalchemy.exc.ArgumentError
527+
528+
# Make sure that errors are propagated from class_mapper when instantiating new classes
529+
error = None
530+
try:
531+
class ArticleTwo(SQLAlchemyObjectType):
532+
class Meta(object):
533+
model = Article
534+
except sqlalchemy.exc.ArgumentError as e:
535+
error = e
536+
537+
# Check that an error occured, and that it was the unique error we gave
538+
assert error is not None
539+
assert isinstance(error, sqlalchemy.exc.ArgumentError)
540+
541+
542+
@mock.patch(utils.__name__ + '.class_mapper')
543+
def test_unmapped_errors_reformat(class_mapper_mock):
544+
# Mock class_mapper effect
545+
class_mapper_mock.side_effect = sqlalchemy.orm.exc.UnmappedClassError(object)
546+
547+
# Make sure that errors are propagated from class_mapper when instantiating new classes
548+
error = None
549+
try:
550+
class ArticleThree(SQLAlchemyObjectType):
551+
class Meta(object):
552+
model = Article
553+
except ValueError as e:
554+
error = e
555+
556+
# Check that an error occured, and that it was the unique error we gave
557+
assert error is not None
558+
assert isinstance(error, ValueError)
559+
assert "You need to pass a valid SQLAlchemy Model" in str(error)

Diff for: ‎graphene_sqlalchemy/types.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,11 @@ def __init_subclass_with_meta__(
207207
_meta=None,
208208
**options
209209
):
210-
assert is_mapped_class(model), (
211-
"You need to pass a valid SQLAlchemy Model in " '{}.Meta, received "{}".'
212-
).format(cls.__name__, model)
210+
# Make sure model is a valid SQLAlchemy model
211+
if not is_mapped_class(model):
212+
raise ValueError(
213+
"You need to pass a valid SQLAlchemy Model in " '{}.Meta, received "{}".'.format(cls.__name__, model)
214+
)
213215

214216
if not registry:
215217
registry = get_global_registry()

Diff for: ‎graphene_sqlalchemy/utils.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ def get_query(model, context):
2727
def is_mapped_class(cls):
2828
try:
2929
class_mapper(cls)
30-
except (ArgumentError, UnmappedClassError):
30+
except ArgumentError as error:
31+
# Only handle ArgumentErrors for non-class objects
32+
if "Class object expected" in str(error):
33+
return False
34+
raise
35+
except UnmappedClassError:
36+
# Unmapped classes return false
3137
return False
3238
else:
3339
return True

0 commit comments

Comments
 (0)