diff --git a/.travis.yml b/.travis.yml index 76a4dc6f..713d720e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,3 +57,4 @@ deploy: 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/README.md b/README.md index 7178e99f..876381d5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -Please read [UPGRADE-v1.0.md](https://github.com/graphql-python/graphene/blob/master/UPGRADE-v1.0.md) -to learn how to upgrade to Graphene `1.0`. +Please read [UPGRADE-v2.0.md](https://github.com/graphql-python/graphene/blob/master/UPGRADE-v2.0.md) +to learn how to upgrade to Graphene `2.0`. --- @@ -13,7 +13,7 @@ A [SQLAlchemy](http://www.sqlalchemy.org/) integration for [Graphene](http://gra For instaling graphene, just run this command in your shell ```bash -pip install "graphene-sqlalchemy>=1.0" +pip install "graphene-sqlalchemy>=2.0" ``` ## Examples @@ -47,8 +47,8 @@ class User(SQLAlchemyObjectType): class Query(graphene.ObjectType): users = graphene.List(User) - def resolve_users(self, args, context, info): - query = User.get_query(context) # SQLAlchemy query + def resolve_users(self, info): + query = User.get_query(info.context) # SQLAlchemy query return query.all() schema = graphene.Schema(query=Query) diff --git a/README.rst b/README.rst index e1983a2a..e367d430 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ Please read -`UPGRADE-v1.0.md `__ -to learn how to upgrade to Graphene ``1.0``. +`UPGRADE-v2.0.md `__ +to learn how to upgrade to Graphene ``2.0``. -------------- @@ -17,7 +17,7 @@ For instaling graphene, just run this command in your shell .. code:: bash - pip install "graphene-sqlalchemy>=1.0" + pip install "graphene-sqlalchemy>=2.0" Examples -------- @@ -53,8 +53,8 @@ following: class Query(graphene.ObjectType): users = graphene.List(User) - def resolve_users(self, args, context, info): - query = User.get_query(context) # SQLAlchemy query + def resolve_users(self, info): + query = User.get_query(info.context) # SQLAlchemy query return query.all() schema = graphene.Schema(query=Query) diff --git a/graphene_sqlalchemy/__init__.py b/graphene_sqlalchemy/__init__.py index 5c6c0b38..88cbeb43 100644 --- a/graphene_sqlalchemy/__init__.py +++ b/graphene_sqlalchemy/__init__.py @@ -9,7 +9,12 @@ get_session ) -__all__ = ['SQLAlchemyObjectType', - 'SQLAlchemyConnectionField', - 'get_query', - 'get_session'] +__version__ = '2.0.dev2017073101' + +__all__ = [ + '__version__', + 'SQLAlchemyObjectType', + 'SQLAlchemyConnectionField', + 'get_query', + 'get_session' +] diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index 0aaa68eb..17e54b0f 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -5,7 +5,6 @@ from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List, String) -from graphene.relay import is_node from graphene.types.json import JSONString from .fields import createConnectionField @@ -43,15 +42,17 @@ def dynamic_type(): return Field(_type) elif (direction == interfaces.ONETOMANY or direction == interfaces.MANYTOMANY): - if is_node(_type): + if _type._meta.connection: return createConnectionField(_type) return Field(List(_type)) return Dynamic(dynamic_type) - + + def convert_sqlalchemy_hybrid_method(hybrid_item): - return String(description=getattr(hybrid_item, '__doc__', None), - required=False) + return String(description=getattr(hybrid_item, '__doc__', None), + required=False) + def convert_sqlalchemy_composite(composite, registry): converter = registry.get_converter_for_composite(composite.composite_class) diff --git a/graphene_sqlalchemy/fields.py b/graphene_sqlalchemy/fields.py index df0d186e..bb084b3a 100644 --- a/graphene_sqlalchemy/fields.py +++ b/graphene_sqlalchemy/fields.py @@ -16,14 +16,24 @@ def model(self): return self.type._meta.node._meta.model @classmethod - def get_query(cls, model, context, info, args): - return get_query(model, context) + def get_query(cls, model, info, **args): + return get_query(model, info.context) + + @property + def type(self): + from .types import SQLAlchemyObjectType + _type = super(ConnectionField, self).type + assert issubclass(_type, SQLAlchemyObjectType), ( + "SQLAlchemyConnectionField only accepts SQLAlchemyObjectType types" + ) + assert _type._meta.connection, "The type {} doesn't have a connection".format(_type.__name__) + return _type._meta.connection @classmethod - def connection_resolver(cls, resolver, connection, model, root, args, context, info): - iterable = resolver(root, args, context, info) + def connection_resolver(cls, resolver, connection, model, root, info, **args): + iterable = resolver(root, info, **args) if iterable is None: - iterable = cls.get_query(model, context, info, args) + iterable = cls.get_query(model, info, **args) if isinstance(iterable, Query): _len = iterable.count() else: diff --git a/graphene_sqlalchemy/registry.py b/graphene_sqlalchemy/registry.py index 091aaf25..61285cbf 100644 --- a/graphene_sqlalchemy/registry.py +++ b/graphene_sqlalchemy/registry.py @@ -6,9 +6,9 @@ def __init__(self): self._registry_composites = {} def register(self, cls): - from .types import SQLAlchemyObjectTypeMeta - assert issubclass(type(cls), SQLAlchemyObjectTypeMeta), ( - 'Only classes of type SQLAlchemyObjectTypeMeta can be registered, ', + from .types import SQLAlchemyObjectType + assert issubclass(cls, SQLAlchemyObjectType), ( + 'Only classes of type SQLAlchemyObjectType can be registered, ', 'received "{}"' ).format(cls.__name__) assert cls._meta.registry == self, 'Registry for a Model have to match.' diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index b0da85a0..3f27bc48 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -34,6 +34,12 @@ class Reporter(Base): articles = relationship('Article', backref='reporter') favorite_article = relationship("Article", uselist=False) + # total = column_property( + # select([ + # func.cast(func.count(PersonInfo.id), Float) + # ]) + # ) + class Article(Base): __tablename__ = 'articles' @@ -43,8 +49,11 @@ class Article(Base): reporter_id = Column(Integer(), ForeignKey('reporters.id')) -class ReflectedEditor: +class ReflectedEditor(type): """Same as Editor, but using reflected table.""" + @classmethod + def __subclasses__(cls): + return [] editor_table = Table('editors', Base.metadata, autoload=True) diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index c43b8919..3c732b27 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -1,8 +1,8 @@ from py.test import raises -from sqlalchemy import Column, Table, case, types +from sqlalchemy import Column, Table, case, types, select, func from sqlalchemy.dialects import postgresql from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import composite +from sqlalchemy.orm import composite, column_property from sqlalchemy.sql.elements import Label from sqlalchemy_utils import ChoiceType, JSONType, ScalarListType @@ -136,6 +136,23 @@ def test_should_choice_convert_enum(): assert graphene_type._meta.enum.__members__['en'].value == 'English' +def test_should_columproperty_convert(): + + Base = declarative_base() + + class Test(Base): + __tablename__ = 'test' + id = Column(types.Integer, primary_key=True) + column = column_property( + select([func.sum(func.cast(id, types.Integer))]).where( + id==1 + ) + ) + + graphene_type = convert_sqlalchemy_column(Test.column) + assert graphene_type.kwargs['required'] == False + + def test_should_scalar_list_convert_list(): assert_column_conversion(ScalarListType(), graphene.List) diff --git a/graphene_sqlalchemy/tests/test_query.py b/graphene_sqlalchemy/tests/test_query.py index 042352c8..e4c3f835 100644 --- a/graphene_sqlalchemy/tests/test_query.py +++ b/graphene_sqlalchemy/tests/test_query.py @@ -270,18 +270,17 @@ class Meta: class CreateArticle(graphene.Mutation): - class Input: + class Arguments: headline = graphene.String() reporter_id = graphene.ID() ok = graphene.Boolean() article = graphene.Field(ArticleNode) - @classmethod - def mutate(cls, instance, args, context, info): + def mutate(self, info, headline, reporter_id): new_article = Article( - headline=args.get('headline'), - reporter_id=args.get('reporter_id'), + headline=headline, + reporter_id=reporter_id, ) session.add(new_article) diff --git a/graphene_sqlalchemy/tests/test_types.py b/graphene_sqlalchemy/tests/test_types.py index 25476b10..3f017aae 100644 --- a/graphene_sqlalchemy/tests/test_types.py +++ b/graphene_sqlalchemy/tests/test_types.py @@ -4,7 +4,7 @@ import six from ..registry import Registry -from ..types import SQLAlchemyObjectType, SQLAlchemyObjectTypeMeta +from ..types import SQLAlchemyObjectType from .models import Article, Reporter registry = Registry() @@ -72,8 +72,21 @@ def test_node_replacedfield(): def test_object_type(): + + + class Human(SQLAlchemyObjectType): + '''Human description''' + + pub_date = Int() + + class Meta: + model = Article + # exclude_fields = ('id', ) + registry = registry + interfaces = (Node, ) + assert issubclass(Human, ObjectType) - assert list(Human._meta.fields.keys()) == ['id', 'headline', 'reporter_id', 'reporter', 'pub_date'] + assert list(Human._meta.fields.keys()) == ['id', 'headline', 'pub_date', 'reporter_id', 'reporter'] assert is_node(Human) diff --git a/graphene_sqlalchemy/tests/test_utils.py b/graphene_sqlalchemy/tests/test_utils.py index 484b9f6a..8af3c61e 100644 --- a/graphene_sqlalchemy/tests/test_utils.py +++ b/graphene_sqlalchemy/tests/test_utils.py @@ -9,8 +9,8 @@ def test_get_session(): class Query(ObjectType): x = String() - def resolve_x(self, args, context, info): - return get_session(context) + def resolve_x(self, info): + return get_session(info.context) query = ''' query ReporterQuery { diff --git a/graphene_sqlalchemy/types.py b/graphene_sqlalchemy/types.py index 0c6add77..44bf9c27 100644 --- a/graphene_sqlalchemy/types.py +++ b/graphene_sqlalchemy/types.py @@ -1,16 +1,13 @@ from collections import OrderedDict -import six from sqlalchemy.inspection import inspect as sqlalchemyinspect from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm.exc import NoResultFound -from graphene import Field, ObjectType -from graphene.relay import is_node -from graphene.types.objecttype import ObjectTypeMeta -from graphene.types.options import Options -from graphene.types.utils import merge, yank_fields_from_attrs -from graphene.utils.is_base_type import is_base_type +from graphene import Field # , annotate, ResolveInfo +from graphene.relay import Connection, Node +from graphene.types.objecttype import ObjectType, ObjectTypeOptions +from graphene.types.utils import yank_fields_from_attrs from .converter import (convert_sqlalchemy_column, convert_sqlalchemy_composite, @@ -20,33 +17,31 @@ from .utils import get_query, is_mapped_class, is_mapped_instance -def construct_fields(options): - only_fields = options.only_fields - exclude_fields = options.exclude_fields - inspected_model = sqlalchemyinspect(options.model) +def construct_fields(model, registry, only_fields, exclude_fields): + inspected_model = sqlalchemyinspect(model) fields = OrderedDict() for name, column in inspected_model.columns.items(): is_not_in_only = only_fields and name not in only_fields - is_already_created = name in options.fields - is_excluded = name in exclude_fields or is_already_created + # is_already_created = name in options.fields + is_excluded = name in exclude_fields # or is_already_created if is_not_in_only or is_excluded: # We skip this field if we specify only_fields and is not # in there. Or when we excldue this field in exclude_fields continue - converted_column = convert_sqlalchemy_column(column, options.registry) + converted_column = convert_sqlalchemy_column(column, registry) fields[name] = converted_column for name, composite in inspected_model.composites.items(): is_not_in_only = only_fields and name not in only_fields - is_already_created = name in options.fields - is_excluded = name in exclude_fields or is_already_created + # is_already_created = name in options.fields + is_excluded = name in exclude_fields # or is_already_created if is_not_in_only or is_excluded: # We skip this field if we specify only_fields and is not # in there. Or when we excldue this field in exclude_fields continue - converted_composite = convert_sqlalchemy_composite(composite, options.registry) + converted_composite = convert_sqlalchemy_composite(composite, registry) fields[name] = converted_composite for hybrid_item in inspected_model.all_orm_descriptors: @@ -55,95 +50,91 @@ def construct_fields(options): name = hybrid_item.__name__ is_not_in_only = only_fields and name not in only_fields - is_already_created = name in options.fields - is_excluded = name in exclude_fields or is_already_created + # is_already_created = name in options.fields + is_excluded = name in exclude_fields # or is_already_created if is_not_in_only or is_excluded: - # We skip this field if we specify only_fields and is not - # in there. Or when we excldue this field in exclude_fields - + # We skip this field if we specify only_fields and is not + # in there. Or when we excldue this field in exclude_fields continue + converted_hybrid_property = convert_sqlalchemy_hybrid_method( - hybrid_item) + hybrid_item + ) fields[name] = converted_hybrid_property # Get all the columns for the relationships on the model for relationship in inspected_model.relationships: is_not_in_only = only_fields and relationship.key not in only_fields - is_already_created = relationship.key in options.fields - is_excluded = relationship.key in exclude_fields or is_already_created + # is_already_created = relationship.key in options.fields + is_excluded = relationship.key in exclude_fields # or is_already_created if is_not_in_only or is_excluded: # We skip this field if we specify only_fields and is not # in there. Or when we excldue this field in exclude_fields continue - converted_relationship = convert_sqlalchemy_relationship(relationship, options.registry) + converted_relationship = convert_sqlalchemy_relationship(relationship, registry) name = relationship.key fields[name] = converted_relationship return fields -class SQLAlchemyObjectTypeMeta(ObjectTypeMeta): - - @staticmethod - def __new__(cls, name, bases, attrs): - # Also ensure initialization is only performed for subclasses of Model - # (excluding Model class itself). - if not is_base_type(bases, SQLAlchemyObjectTypeMeta): - return type.__new__(cls, name, bases, attrs) - - options = Options( - attrs.pop('Meta', None), - name=name, - description=attrs.pop('__doc__', None), - model=None, - local_fields=None, - only_fields=(), - exclude_fields=(), - id='id', - interfaces=(), - registry=None, - abstract=False, - ) - - cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options)) +class SQLAlchemyObjectTypeOptions(ObjectTypeOptions): + model = None # type: Model + registry = None # type: Registry + connection = None # type: Type[Connection] + id = None # type: str - if options.abstract: - return cls - if not options.registry: - options.registry = get_global_registry() - assert isinstance(options.registry, Registry), ( - 'The attribute registry in {}.Meta needs to be an' - ' instance of Registry, received "{}".' - ).format(name, options.registry) - assert is_mapped_class(options.model), ( +class SQLAlchemyObjectType(ObjectType): + @classmethod + def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False, + only_fields=(), exclude_fields=(), connection=None, + use_connection=None, interfaces=(), id=None, **options): + assert is_mapped_class(model), ( 'You need to pass a valid SQLAlchemy Model in ' '{}.Meta, received "{}".' - ).format(name, options.model) + ).format(cls.__name__, model) + + if not registry: + registry = get_global_registry() - options.registry.register(cls) + assert isinstance(registry, Registry), ( + 'The attribute registry in {} needs to be an instance of ' + 'Registry, received "{}".' + ).format(cls.__name__, registry) - options.sqlalchemy_fields = yank_fields_from_attrs( - construct_fields(options), + sqla_fields = yank_fields_from_attrs( + construct_fields(model, registry, only_fields, exclude_fields), _as=Field, ) - options.fields = merge( - options.interface_fields, - options.sqlalchemy_fields, - options.base_fields, - options.local_fields - ) - return cls + if use_connection is None and interfaces: + use_connection = any((issubclass(interface, Node) for interface in interfaces)) + + if use_connection and not connection: + # We create the connection automatically + connection = Connection.create_type('{}Connection'.format(cls.__name__), node=cls) + + if connection is not None: + assert issubclass(connection, Connection), ( + "The connection must be a Connection. Received {}" + ).format(connection.__name__) + + _meta = SQLAlchemyObjectTypeOptions(cls) + _meta.model = model + _meta.registry = registry + _meta.fields = sqla_fields + _meta.connection = connection + _meta.id = id or 'id' + super(SQLAlchemyObjectType, cls).__init_subclass_with_meta__(_meta=_meta, interfaces=interfaces, **options) -class SQLAlchemyObjectType(six.with_metaclass(SQLAlchemyObjectTypeMeta, ObjectType)): - class Meta: - abstract = True + if not skip_registry: + registry.register(cls) @classmethod - def is_type_of(cls, root, context, info): + def is_type_of(cls, root, info): if isinstance(root, cls): return True if not is_mapped_instance(root): @@ -153,20 +144,18 @@ def is_type_of(cls, root, context, info): return isinstance(root, cls._meta.model) @classmethod - def get_query(cls, context): + def get_query(cls, info): model = cls._meta.model - return get_query(model, context) + return get_query(model, info.context) @classmethod - def get_node(cls, id, context, info): + def get_node(cls, info, id): try: - return cls.get_query(context).get(id) + return cls.get_query(info).get(id) except NoResultFound: return None - def resolve_id(self, args, context, info): - graphene_type = info.parent_type.graphene_type - if is_node(graphene_type): - keys = self.__mapper__.primary_key_from_instance(self) - return tuple(keys) if len(keys) > 1 else keys[0] - return getattr(self, graphene_type._meta.id, None) + def resolve_id(self, info): + # graphene_type = info.parent_type.graphene_type + keys = self.__mapper__.primary_key_from_instance(self) + return tuple(keys) if len(keys) > 1 else keys[0] diff --git a/setup.cfg b/setup.cfg index 558f31d7..d8d54e3f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,3 +24,6 @@ norecursedirs = filterwarnings = error ignore::DeprecationWarning + +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py index 32461852..eaeb2df3 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,18 @@ from setuptools import find_packages, setup +import sys +import ast +import re + +_version_re = re.compile(r'__version__\s+=\s+(.*)') + +with open('graphene_sqlalchemy/__init__.py', 'rb') as f: + version = str(ast.literal_eval(_version_re.search( + f.read().decode('utf-8')).group(1))) + setup( name='graphene-sqlalchemy', - version='1.1.1', + version=version, description='Graphene SQLAlchemy integration', long_description=open('README.rst').read(), @@ -33,7 +43,7 @@ install_requires=[ 'six>=1.10.0', - 'graphene>=1.0', + 'graphene>=2.0.dev', 'SQLAlchemy', 'singledispatch>=3.4.0.3', 'iso8601',