From 36b668488fcfca85379435ef09c4244a2b0926cb Mon Sep 17 00:00:00 2001 From: Sabar Dasgupta Date: Mon, 25 Jul 2022 11:36:00 -0400 Subject: [PATCH 1/5] add filter tests for discussion --- graphene_sqlalchemy/tests/test_filters.py | 317 ++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 graphene_sqlalchemy/tests/test_filters.py diff --git a/graphene_sqlalchemy/tests/test_filters.py b/graphene_sqlalchemy/tests/test_filters.py new file mode 100644 index 00000000..f298bdca --- /dev/null +++ b/graphene_sqlalchemy/tests/test_filters.py @@ -0,0 +1,317 @@ +import graphene + +from ..fields import SQLAlchemyConnectionField +from ..filters import FloatFilter +from ..types import SQLAlchemyObjectType +from .models import Article, Editor, HairKind, Image, Pet, Reporter, Tag +from .utils import to_std_dicts + + +def add_test_data(session): + reporter = Reporter( + first_name='John', last_name='Doe', favorite_pet_kind='cat') + session.add(reporter) + pet = Pet(name='Garfield', pet_kind='cat', hair_kind=HairKind.SHORT) + pet.reporters = reporter + session.add(pet) + pet = Pet(name='Snoopy', pet_kind='dog', hair_kind=HairKind.SHORT) + pet.reporters = reporter + session.add(pet) + reporter = Reporter( + first_name='John', last_name='Woe', favorite_pet_kind='cat') + session.add(reporter) + article = Article(headline='Hi!') + article.reporter = reporter + session.add(article) + reporter = Reporter( + first_name='Jane', last_name='Roe', favorite_pet_kind='dog') + session.add(reporter) + pet = Pet(name='Lassie', pet_kind='dog', hair_kind=HairKind.LONG) + pet.reporters.append(reporter) + session.add(pet) + editor = Editor(name="Jack") + session.add(editor) + session.commit() + + +def create_schema(session): + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + + class ImageType(SQLAlchemyObjectType): + class Meta: + model = Image + + class ReporterType(SQLAlchemyObjectType): + class Meta: + model = Reporter + + class Query(graphene.ObjectType): + article = graphene.Field(ArticleType) + articles = graphene.List(ArticleType) + image = graphene.Field(ImageType) + images = graphene.List(ImageType) + reporter = graphene.Field(ReporterType) + reporters = graphene.List(ReporterType) + + def resolve_article(self, _info): + return session.query(Article).first() + + def resolve_articles(self, _info): + return session.query(Article) + + def resolve_image(self, _info): + return session.query(Image).first() + + def resolve_images(self, _info): + return session.query(Image) + + def resolve_reporter(self, _info): + return session.query(Reporter).first() + + def resolve_reporters(self, _info): + return session.query(Reporter) + + return Query + + +# Test a simple example of filtering +def test_filter_simple(session): + add_test_data(session) + Query = create_schema(session) + + query = """ + query { + reporters(filters: {firstName: "John"}) { + firstName + } + } + """ + expected = { + "reporters": [{"firstName": "John"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test a custom filter type +def test_filter_custom_type(session): + add_test_data(session) + Query = create_schema(session) + + class MathFilter(FloatFilter): + def divisibleBy(dividend, divisor): + return dividend % divisor == 0 + + class ExtraQuery: + pets = SQLAlchemyConnectionField(Pet, filters=MathFilter()) + + class CustomQuery(Query, ExtraQuery): + pass + + query = """ + query { + pets (filters: { + legs: {divisibleBy: 2} + }) { + name + } + } + """ + expected = { + "pets": [{"name": "Garfield"}, {"name": "Lassie"}], + } + schema = graphene.Schema(query=CustomQuery) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + +# Test a 1:1 relationship +def test_filter_relationship_one_to_one(session): + article = Article(headline='Hi!') + image = Image(external_id=1, description="A beautiful image.") + article.image = image + session.add(article) + session.add(image) + session.commit() + + Query = create_schema(session) + + query = """ + query { + article (filters: { + image: {description: "A beautiful image."} + }) { + firstName + } + } + """ + expected = { + "article": [{"headline": "Hi!"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test a 1:n relationship +def test_filter_relationship_one_to_many(session): + add_test_data(session) + Query = create_schema(session) + + query = """ + query { + reporter (filters: { + pets: { + name: {in: ["Garfield", "Snoopy"]} + } + }) { + firstName + lastName + } + } + """ + expected = { + "reporter": [{"firstName": "John"}, {"lastName": "Doe"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test a n:m relationship +def test_filter_relationship_many_to_many(session): + article1 = Article(headline='Article! Look!') + article2 = Article(headline='Woah! Another!') + tag1 = Tag(name="sensational") + tag2 = Tag(name="eye-grabbing") + article1.tags.append(tag1) + article2.tags.append([tag1, tag2]) + session.add(article1) + session.add(article2) + session.add(tag1) + session.add(tag2) + session.commit() + + Query = create_schema(session) + + query = """ + query { + articles (filters: { + tags: { name: { in: ["sensational", "eye-grabbing"] } } + }) { + headline + } + } + """ + expected = { + "articles": [{"headline": "Woah! Another!"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test connecting filters with "and" +def test_filter_logic_and(session): + add_test_data(session) + + Query = create_schema(session) + + query = """ + query { + reporters (filters: { + and: [ + {firstName: "John"}, + {favoritePetKind: "cat"}, + ] + }) { + lastName + } + } + """ + expected = { + "reporters": [{"lastName": "Doe"}, {"lastName": "Woe"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test connecting filters with "or" +def test_filter_logic_or(session): + add_test_data(session) + Query = create_schema(session) + + query = """ + query { + reporters (filters: { + or: [ + {lastName: "Woe"}, + {favoritePetKind: "dog"}, + ] + }) { + firstName + lastName + } + } + """ + expected = { + "reporters": [ + {"firstName": "John", "lastName": "Woe"}, + {"firstName": "Jane", "lastName": "Roe"}, + ], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test connecting filters with "and" and "or" together +def test_filter_logic_and_or(session): + add_test_data(session) + Query = create_schema(session) + + query = """ + query { + reporters (filters: { + and: [ + {firstName: "John"}, + or : [ + {lastName: "Doe"}, + {favoritePetKind: "cat"}, + ] + ] + }) { + firstName + } + } + """ + expected = { + "reporters": [{"firstName": "John"}, {"firstName": "Jane"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# TODO hybrid property +def test_filter_hybrid_property(session): + raise NotImplementedError From 9aae6c777ea870faf31d60911bb54b640b29c4c9 Mon Sep 17 00:00:00 2001 From: Sabar Dasgupta Date: Tue, 2 Aug 2022 13:30:14 -0400 Subject: [PATCH 2/5] add typing for custom filter --- graphene_sqlalchemy/tests/test_filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_filters.py b/graphene_sqlalchemy/tests/test_filters.py index f298bdca..13b58427 100644 --- a/graphene_sqlalchemy/tests/test_filters.py +++ b/graphene_sqlalchemy/tests/test_filters.py @@ -104,8 +104,8 @@ def test_filter_custom_type(session): Query = create_schema(session) class MathFilter(FloatFilter): - def divisibleBy(dividend, divisor): - return dividend % divisor == 0 + def divisibleBy(dividend: float, divisor: float) -> float: + return dividend % divisor == 0. class ExtraQuery: pets = SQLAlchemyConnectionField(Pet, filters=MathFilter()) From e655b21755a0512fce7dd97aa9f903d70c0b6622 Mon Sep 17 00:00:00 2001 From: Sabar Dasgupta Date: Tue, 2 Aug 2022 15:03:37 -0400 Subject: [PATCH 3/5] update 1:n and n:m filter tests to use RelationshipFilter syntax --- graphene_sqlalchemy/tests/test_filters.py | 106 +++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/graphene_sqlalchemy/tests/test_filters.py b/graphene_sqlalchemy/tests/test_filters.py index 13b58427..584bde76 100644 --- a/graphene_sqlalchemy/tests/test_filters.py +++ b/graphene_sqlalchemy/tests/test_filters.py @@ -166,11 +166,38 @@ def test_filter_relationship_one_to_many(session): add_test_data(session) Query = create_schema(session) + # test contains query = """ query { reporter (filters: { pets: { - name: {in: ["Garfield", "Snoopy"]} + contains: { + name: {in: ["Garfield", "Lassie"]} + } + } + }) { + lastName + } + } + """ + expected = { + "reporter": [{"lastName": "Doe"}, {"lastName": "Roe"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + # test containsAllOf + query = """ + query { + reporter (filters: { + pets: { + containsAllOf: [ + name: {eq: "Garfield"}, + name: {eq: "Snoopy"}, + ] } }) { firstName @@ -187,6 +214,28 @@ def test_filter_relationship_one_to_many(session): result = to_std_dicts(result.data) assert result == expected + # test containsExactly + query = """ + query { + reporter (filters: { + pets: { + containsExactly: [ + name: {eq: "Garfield"} + ] + } + }) { + firstName + } + } + """ + expected = { + "reporter": [], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected # Test a n:m relationship def test_filter_relationship_many_to_many(session): @@ -204,10 +253,42 @@ def test_filter_relationship_many_to_many(session): Query = create_schema(session) + # test contains + query = """ + query { + articles (filters: { + tags: { + contains: { + name: { in: ["sensational", "eye-grabbing"] } + } + } + }) { + headline + } + } + """ + expected = { + "articles": [ + {"headline": "Woah! Another!"}, + {"headline": "Article! Look!"}, + ], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + # test containsAllOf query = """ query { articles (filters: { - tags: { name: { in: ["sensational", "eye-grabbing"] } } + tags: { + containsAllOf: [ + { tag: { name: { eq: "eye-grabbing" } } }, + { tag: { name: { eq: "sensational" } } }, + ] + } }) { headline } @@ -222,6 +303,27 @@ def test_filter_relationship_many_to_many(session): result = to_std_dicts(result.data) assert result == expected + # test containsExactly + query = """ + query { + articles (filters: { + containsExactly: [ + { tag: { name: { eq: "sensational" } } } + ] + }) { + headline + } + } + """ + expected = { + "articles": [{"headline": "Article! Look!"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + # Test connecting filters with "and" def test_filter_logic_and(session): From 8f4a38005c01deee3e02bbf838500fe14191b9c3 Mon Sep 17 00:00:00 2001 From: Sabar Dasgupta Date: Tue, 2 Aug 2022 15:41:30 -0400 Subject: [PATCH 4/5] add models and mark tests to fail --- graphene_sqlalchemy/tests/models.py | 31 ++++++++++++++++++++++- graphene_sqlalchemy/tests/test_filters.py | 10 ++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index 88e992b9..de786c52 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -6,7 +6,7 @@ func, select) from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import column_property, composite, mapper, relationship +from sqlalchemy.orm import backref, column_property, composite, mapper, relationship PetKind = Enum("cat", "dog", name="pet_kind") @@ -39,6 +39,7 @@ class Pet(Base): pet_kind = Column(PetKind, nullable=False) hair_kind = Column(Enum(HairKind, name="hair_kind"), nullable=False) reporter_id = Column(Integer(), ForeignKey("reporters.id")) + legs = Column(Integer(), default=4) class CompositeFullName(object): @@ -76,6 +77,14 @@ def hybrid_prop(self): composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite") +articles_tags_table = Table( + "articles_tags", + Base.metadata, + Column("article_id", ForeignKey("article.id")), + Column("imgae_id", ForeignKey("image.id")), +) + + class Article(Base): __tablename__ = "articles" id = Column(Integer(), primary_key=True) @@ -83,6 +92,26 @@ class Article(Base): pub_date = Column(Date()) reporter_id = Column(Integer(), ForeignKey("reporters.id")) + # one-to-one relationship with image + image_id = Column(Integer(), ForeignKey('image.id'), unique=True) + image = relationship("Image", backref=backref("articles", uselist=False)) + + # many-to-many relationship with tags + tags = relationship("Tag", secondary=articles_tags_table, backref="articles") + + +class Image(Base): + __tablename__ = "images" + id = Column(Integer(), primary_key=True) + external_id = Column(Integer()) + description = Column(String(30)) + + +class Tag(Base): + __tablename__ = "tags" + id = Column(Integer(), primary_key=True) + name = Column(String(30)) + class ReflectedEditor(type): """Same as Editor, but using reflected table.""" diff --git a/graphene_sqlalchemy/tests/test_filters.py b/graphene_sqlalchemy/tests/test_filters.py index 584bde76..720f84ed 100644 --- a/graphene_sqlalchemy/tests/test_filters.py +++ b/graphene_sqlalchemy/tests/test_filters.py @@ -1,4 +1,5 @@ import graphene +import pytest from ..fields import SQLAlchemyConnectionField from ..filters import FloatFilter @@ -77,6 +78,7 @@ def resolve_reporters(self, _info): # Test a simple example of filtering +@pytest.mark.xfail def test_filter_simple(session): add_test_data(session) Query = create_schema(session) @@ -99,6 +101,7 @@ def test_filter_simple(session): # Test a custom filter type +@pytest.mark.xfail def test_filter_custom_type(session): add_test_data(session) Query = create_schema(session) @@ -132,6 +135,7 @@ class CustomQuery(Query, ExtraQuery): assert result == expected # Test a 1:1 relationship +@pytest.mark.xfail def test_filter_relationship_one_to_one(session): article = Article(headline='Hi!') image = Image(external_id=1, description="A beautiful image.") @@ -162,6 +166,7 @@ def test_filter_relationship_one_to_one(session): # Test a 1:n relationship +@pytest.mark.xfail def test_filter_relationship_one_to_many(session): add_test_data(session) Query = create_schema(session) @@ -238,6 +243,7 @@ def test_filter_relationship_one_to_many(session): assert result == expected # Test a n:m relationship +@pytest.mark.xfail def test_filter_relationship_many_to_many(session): article1 = Article(headline='Article! Look!') article2 = Article(headline='Woah! Another!') @@ -326,6 +332,7 @@ def test_filter_relationship_many_to_many(session): # Test connecting filters with "and" +@pytest.mark.xfail def test_filter_logic_and(session): add_test_data(session) @@ -354,6 +361,7 @@ def test_filter_logic_and(session): # Test connecting filters with "or" +@pytest.mark.xfail def test_filter_logic_or(session): add_test_data(session) Query = create_schema(session) @@ -385,6 +393,7 @@ def test_filter_logic_or(session): # Test connecting filters with "and" and "or" together +@pytest.mark.xfail def test_filter_logic_and_or(session): add_test_data(session) Query = create_schema(session) @@ -415,5 +424,6 @@ def test_filter_logic_and_or(session): # TODO hybrid property +@pytest.mark.xfail def test_filter_hybrid_property(session): raise NotImplementedError From 124a66b31d38c3d50fa9dda51606868c6e22887c Mon Sep 17 00:00:00 2001 From: Sabar Dasgupta Date: Tue, 2 Aug 2022 16:02:35 -0400 Subject: [PATCH 5/5] make filter tests run --- graphene_sqlalchemy/filters.py | 2 ++ graphene_sqlalchemy/tests/models.py | 32 ++++++++++---------- graphene_sqlalchemy/tests/test_filters.py | 4 +-- graphene_sqlalchemy/tests/test_sort_enums.py | 10 +++++- 4 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 graphene_sqlalchemy/filters.py diff --git a/graphene_sqlalchemy/filters.py b/graphene_sqlalchemy/filters.py new file mode 100644 index 00000000..6bfe7c96 --- /dev/null +++ b/graphene_sqlalchemy/filters.py @@ -0,0 +1,2 @@ +class FloatFilter: + pass diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index de786c52..4cf2c3c8 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -80,11 +80,24 @@ def hybrid_prop(self): articles_tags_table = Table( "articles_tags", Base.metadata, - Column("article_id", ForeignKey("article.id")), - Column("imgae_id", ForeignKey("image.id")), + Column("article_id", ForeignKey("articles.id")), + Column("tag_id", ForeignKey("tags.id")), ) +class Image(Base): + __tablename__ = "images" + id = Column(Integer(), primary_key=True) + external_id = Column(Integer()) + description = Column(String(30)) + + +class Tag(Base): + __tablename__ = "tags" + id = Column(Integer(), primary_key=True) + name = Column(String(30)) + + class Article(Base): __tablename__ = "articles" id = Column(Integer(), primary_key=True) @@ -93,26 +106,13 @@ class Article(Base): reporter_id = Column(Integer(), ForeignKey("reporters.id")) # one-to-one relationship with image - image_id = Column(Integer(), ForeignKey('image.id'), unique=True) + image_id = Column(Integer(), ForeignKey('images.id'), unique=True) image = relationship("Image", backref=backref("articles", uselist=False)) # many-to-many relationship with tags tags = relationship("Tag", secondary=articles_tags_table, backref="articles") -class Image(Base): - __tablename__ = "images" - id = Column(Integer(), primary_key=True) - external_id = Column(Integer()) - description = Column(String(30)) - - -class Tag(Base): - __tablename__ = "tags" - id = Column(Integer(), primary_key=True) - name = Column(String(30)) - - class ReflectedEditor(type): """Same as Editor, but using reflected table.""" diff --git a/graphene_sqlalchemy/tests/test_filters.py b/graphene_sqlalchemy/tests/test_filters.py index 720f84ed..f44b139c 100644 --- a/graphene_sqlalchemy/tests/test_filters.py +++ b/graphene_sqlalchemy/tests/test_filters.py @@ -51,8 +51,8 @@ class Meta: class Query(graphene.ObjectType): article = graphene.Field(ArticleType) articles = graphene.List(ArticleType) - image = graphene.Field(ImageType) - images = graphene.List(ImageType) + # image = graphene.Field(ImageType) + # images = graphene.List(ImageType) reporter = graphene.Field(ReporterType) reporters = graphene.List(ReporterType) diff --git a/graphene_sqlalchemy/tests/test_sort_enums.py b/graphene_sqlalchemy/tests/test_sort_enums.py index 6291d4f8..64777dbc 100644 --- a/graphene_sqlalchemy/tests/test_sort_enums.py +++ b/graphene_sqlalchemy/tests/test_sort_enums.py @@ -40,6 +40,8 @@ class Meta: "HAIR_KIND_DESC", "REPORTER_ID_ASC", "REPORTER_ID_DESC", + "LEGS_ASC", + "LEGS_DESC", ] assert str(sort_enum.ID_ASC.value.value) == "pets.id ASC" assert str(sort_enum.ID_DESC.value.value) == "pets.id DESC" @@ -94,6 +96,8 @@ class Meta: "PET_KIND_DESC", "HAIR_KIND_ASC", "HAIR_KIND_DESC", + "LEGS_ASC", + "LEGS_DESC", ] @@ -134,6 +138,8 @@ class Meta: "HAIR_KIND_DESC", "REPORTER_ID_ASC", "REPORTER_ID_DESC", + "LEGS_ASC", + "LEGS_DESC", ] assert str(sort_enum.ID_ASC.value.value) == "pets.id ASC" assert str(sort_enum.ID_DESC.value.value) == "pets.id DESC" @@ -148,7 +154,7 @@ def test_sort_argument_with_excluded_fields_in_object_type(): class PetType(SQLAlchemyObjectType): class Meta: model = Pet - exclude_fields = ["hair_kind", "reporter_id"] + exclude_fields = ["hair_kind", "reporter_id", "legs"] sort_arg = PetType.sort_argument() sort_enum = sort_arg.type._of_type @@ -237,6 +243,8 @@ def get_symbol_name(column_name, sort_asc=True): "HairKindDown", "ReporterIdUp", "ReporterIdDown", + "LegsUp", + "LegsDown", ] assert sort_arg.default_value == ["IdUp"]