From 7a776802e39f8e3c1a4a176977f4bf1c017217b5 Mon Sep 17 00:00:00 2001 From: Devin Fee Date: Fri, 2 Jun 2017 14:27:32 -0700 Subject: [PATCH 1/4] Allow subclassing while still protecting the registry --- graphene_sqlalchemy/registry.py | 9 +++++---- setup.cfg | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/graphene_sqlalchemy/registry.py b/graphene_sqlalchemy/registry.py index 63499b8b..48e5f2d1 100644 --- a/graphene_sqlalchemy/registry.py +++ b/graphene_sqlalchemy/registry.py @@ -6,10 +6,11 @@ def __init__(self): self._registry_composites = {} def register(self, cls): - from .types import SQLAlchemyObjectType - assert issubclass( - cls, SQLAlchemyObjectType), 'Only SQLAlchemyObjectType can be registered, received "{}"'.format( - cls.__name__) + from .types import SQLAlchemyObjectType, SQLAlchemyObjectTypeMeta + assert issubclass(type(cls), SQLAlchemyObjectTypeMeta), ( + 'Only classes of type SQLAlchemyObjectTypeMeta can be registered, ', + 'received "{}"' + ).format(cls.__name__) assert cls._meta.registry == self, 'Registry for a Model have to match.' # assert self.get_type_for_model(cls._meta.model) in [None, cls], ( # 'SQLAlchemy model "{}" already associated with ' diff --git a/setup.cfg b/setup.cfg index a42ec1c9..558f31d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,3 +7,20 @@ omit = */tests/* [isort] known_first_party=graphene,graphene_sqlalchemy + +[tool:pytest] +testpaths = graphene_sqlalchemy/ +addopts = + -s + ; --cov graphene-sqlalchemy +norecursedirs = + __pycache__ + *.egg-info + .cache + .git + .tox + appdir + docs +filterwarnings = + error + ignore::DeprecationWarning From 0657bae040d9ea9a1fe51229146992da8ddbe541 Mon Sep 17 00:00:00 2001 From: Devin Fee Date: Fri, 2 Jun 2017 14:39:06 -0700 Subject: [PATCH 2/4] added additional tests for CustomSQLAlchemyObjectType --- graphene_sqlalchemy/tests/test_types.py | 46 ++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index 83bdc263..397ab243 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -1,9 +1,10 @@ from graphene import Field, Int, Interface, ObjectType from graphene.relay import Node, is_node +import six from ..registry import Registry -from ..types import SQLAlchemyObjectType +from ..types import SQLAlchemyObjectType, SQLAlchemyObjectTypeMeta from .models import Article, Reporter registry = Registry() @@ -74,3 +75,46 @@ def test_object_type(): assert issubclass(Human, ObjectType) assert list(Human._meta.fields.keys()) == ['id', 'headline', 'reporter_id', 'reporter', 'pub_date'] assert is_node(Human) + + + +# Test Custom SQLAlchemyObjectType Implementation +class CustomSQLAlchemyObjectType(six.with_metaclass(SQLAlchemyObjectTypeMeta, ObjectType)): + @classmethod + def is_type_of(cls, root, context, info): + return SQLAlchemyObjectType.is_type_of(root, context, info) + + @classmethod + def get_query(cls, context): + return SQLAlchemyObjectType.get_query(context) + + @classmethod + def get_node(cls, id, context, info): + return SQLAlchemyObjectType.get_node(id, context, info) + + @classmethod + def resolve_id(self, args, context, info): + graphene_type = info.parent_type.graphene_type + if is_node(graphene_type): + return self.__mapper__.primary_key_from_instance(self)[0] + return getattr(self, graphene_type._meta.id, None) + + +class CustomCharacter(CustomSQLAlchemyObjectType): + '''Character description''' + class Meta: + model = Reporter + registry = registry + +def test_custom_objecttype_registered(): + assert issubclass(CustomCharacter, ObjectType) + assert CustomCharacter._meta.model == Reporter + assert list( + CustomCharacter._meta.fields.keys()) == [ + 'id', + 'first_name', + 'last_name', + 'email', + 'pets', + 'articles', + 'favorite_article'] From 0cd2ccf9498fe4aea4867b8adb8be169e70ccc5e Mon Sep 17 00:00:00 2001 From: Devin Fee Date: Fri, 2 Jun 2017 15:12:52 -0700 Subject: [PATCH 3/4] added tests, updated docs, allow for subclassing SQLAlchemyObjectType --- README.md | 32 +++++++++++++++++++++++++ graphene_sqlalchemy/tests/test_types.py | 23 ++++-------------- graphene_sqlalchemy/types.py | 12 +++++++--- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 283f947f..7178e99f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,38 @@ query = ''' result = schema.execute(query, context_value={'session': db_session}) ``` +You may also subclass SQLAlchemyObjectType by providing `abstract = True` in +your subclasses Meta: +```python +from graphene_sqlalchemy import SQLAlchemyObjectType + +class ActiveSQLAlchemyObjectType(SQLAlchemyObjectType): + class Meta: + abstract = True + + @classmethod + def get_node(cls, id, context, info): + return cls.get_query(context).\ + filter(and_( + cls._meta.model.deleted_at==None, + cls._meta.model.id==id, + )).\ + first() + +class User(ActiveSQLAlchemyObjectType): + class Meta: + model = UserModel + +class Query(graphene.ObjectType): + users = graphene.List(User) + + def resolve_users(self, args, context, info): + query = User.get_query(context) # SQLAlchemy query + return query.all() + +schema = graphene.Schema(query=Query) +``` + To learn more check out the following [examples](examples/): * **Full example**: [Flask SQLAlchemy example](examples/flask_sqlalchemy) diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index 397ab243..25476b10 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -79,25 +79,9 @@ def test_object_type(): # Test Custom SQLAlchemyObjectType Implementation -class CustomSQLAlchemyObjectType(six.with_metaclass(SQLAlchemyObjectTypeMeta, ObjectType)): - @classmethod - def is_type_of(cls, root, context, info): - return SQLAlchemyObjectType.is_type_of(root, context, info) - - @classmethod - def get_query(cls, context): - return SQLAlchemyObjectType.get_query(context) - - @classmethod - def get_node(cls, id, context, info): - return SQLAlchemyObjectType.get_node(id, context, info) - - @classmethod - def resolve_id(self, args, context, info): - graphene_type = info.parent_type.graphene_type - if is_node(graphene_type): - return self.__mapper__.primary_key_from_instance(self)[0] - return getattr(self, graphene_type._meta.id, None) +class CustomSQLAlchemyObjectType(SQLAlchemyObjectType): + class Meta: + abstract = True class CustomCharacter(CustomSQLAlchemyObjectType): @@ -106,6 +90,7 @@ class Meta: model = Reporter registry = registry + def test_custom_objecttype_registered(): assert issubclass(CustomCharacter, ObjectType) assert CustomCharacter._meta.model == Reporter diff --git a/graphene_sqlalchemy/types.py b/graphene_sqlalchemy/types.py index 70729ed6..1b9ad2e8 100644 --- a/graphene_sqlalchemy/types.py +++ b/graphene_sqlalchemy/types.py @@ -82,9 +82,15 @@ def __new__(cls, name, bases, attrs): exclude_fields=(), id='id', interfaces=(), - registry=None + registry=None, + abstract=False, ) + cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options)) + + if options.abstract: + return cls + if not options.registry: options.registry = get_global_registry() assert isinstance(options.registry, Registry), ( @@ -96,8 +102,6 @@ def __new__(cls, name, bases, attrs): '{}.Meta, received "{}".' ).format(name, options.model) - cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options)) - options.registry.register(cls) options.sqlalchemy_fields = yank_fields_from_attrs( @@ -115,6 +119,8 @@ def __new__(cls, name, bases, attrs): class SQLAlchemyObjectType(six.with_metaclass(SQLAlchemyObjectTypeMeta, ObjectType)): + class Meta: + abstract = True @classmethod def is_type_of(cls, root, context, info): From 37d43318291f89699ec675ebbc996eaab46af89a Mon Sep 17 00:00:00 2001 From: Devin Fee Date: Tue, 6 Jun 2017 09:45:36 -0700 Subject: [PATCH 4/4] removed unnecessary import --- graphene_sqlalchemy/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/registry.py b/graphene_sqlalchemy/registry.py index 48e5f2d1..091aaf25 100644 --- a/graphene_sqlalchemy/registry.py +++ b/graphene_sqlalchemy/registry.py @@ -6,7 +6,7 @@ def __init__(self): self._registry_composites = {} def register(self, cls): - from .types import SQLAlchemyObjectType, SQLAlchemyObjectTypeMeta + from .types import SQLAlchemyObjectTypeMeta assert issubclass(type(cls), SQLAlchemyObjectTypeMeta), ( 'Only classes of type SQLAlchemyObjectTypeMeta can be registered, ', 'received "{}"'