Skip to content

Commit d3a4320

Browse files
authored
feat!: Stricter non-null fields for relationships (#367)
to-many relationships are now non-null by default. (List[MyType] -> List[MyType!]!) The behavior can be adjusted back to legacy using `converter.set_non_null_many_relationships(False)` or using an `ORMField` manually setting the type for more granular Adjustments
1 parent 2041835 commit d3a4320

File tree

3 files changed

+80
-3
lines changed

3 files changed

+80
-3
lines changed

graphene_sqlalchemy/converter.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,39 @@
5959

6060
is_selectin_available = getattr(strategies, "SelectInLoader", None)
6161

62+
"""
63+
Flag for whether to generate stricter non-null fields for many-relationships.
64+
65+
For many-relationships, both the list element and the list field itself will be
66+
non-null by default. This better matches ORM semantics, where there is always a
67+
list for a many relationship (even if it is empty), and it never contains None.
68+
69+
This option can be set to False to revert to pre-3.0 behavior.
70+
71+
For example, given a User model with many Comments:
72+
73+
class User(Base):
74+
comments = relationship("Comment")
75+
76+
The Schema will be:
77+
78+
type User {
79+
comments: [Comment!]!
80+
}
81+
82+
When set to False, the pre-3.0 behavior gives:
83+
84+
type User {
85+
comments: [Comment]
86+
}
87+
"""
88+
use_non_null_many_relationships = True
89+
90+
91+
def set_non_null_many_relationships(non_null_flag):
92+
global use_non_null_many_relationships
93+
use_non_null_many_relationships = non_null_flag
94+
6295

6396
def get_column_doc(column):
6497
return getattr(column, "doc", None)
@@ -160,7 +193,14 @@ def _convert_o2m_or_m2m_relationship(
160193
)
161194

162195
if not child_type._meta.connection:
163-
return graphene.Field(graphene.List(child_type), **field_kwargs)
196+
# check if we need to use non-null fields
197+
list_type = (
198+
graphene.NonNull(graphene.List(graphene.NonNull(child_type)))
199+
if use_non_null_many_relationships
200+
else graphene.List(child_type)
201+
)
202+
203+
return graphene.Field(list_type, **field_kwargs)
164204

165205
# TODO Allow override of connection_field_factory and resolver via ORMField
166206
if connection_field_factory is None:

graphene_sqlalchemy/tests/test_converter.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
convert_sqlalchemy_hybrid_method,
2222
convert_sqlalchemy_relationship,
2323
convert_sqlalchemy_type,
24+
set_non_null_many_relationships,
2425
)
2526
from ..fields import UnsortedSQLAlchemyConnectionField, default_connection_field_factory
2627
from ..registry import Registry, get_global_registry
@@ -71,6 +72,16 @@ class Model(declarative_base()):
7172
)
7273

7374

75+
@pytest.fixture
76+
def use_legacy_many_relationships():
77+
set_non_null_many_relationships(False)
78+
try:
79+
yield
80+
finally:
81+
set_non_null_many_relationships(True)
82+
83+
84+
7485
def test_hybrid_prop_int():
7586
@hybrid_property
7687
def prop_method() -> int:
@@ -501,6 +512,30 @@ class Meta:
501512
True,
502513
"orm_field_name",
503514
)
515+
# field should be [A!]!
516+
assert isinstance(dynamic_field, graphene.Dynamic)
517+
graphene_type = dynamic_field.get_type()
518+
assert isinstance(graphene_type, graphene.Field)
519+
assert isinstance(graphene_type.type, graphene.NonNull)
520+
assert isinstance(graphene_type.type.of_type, graphene.List)
521+
assert isinstance(graphene_type.type.of_type.of_type, graphene.NonNull)
522+
assert graphene_type.type.of_type.of_type.of_type == A
523+
524+
525+
@pytest.mark.usefixtures("use_legacy_many_relationships")
526+
def test_should_manytomany_convert_connectionorlist_list_legacy():
527+
class A(SQLAlchemyObjectType):
528+
class Meta:
529+
model = Pet
530+
531+
dynamic_field = convert_sqlalchemy_relationship(
532+
Reporter.pets.property,
533+
A,
534+
default_connection_field_factory,
535+
True,
536+
"orm_field_name",
537+
)
538+
# field should be [A]
504539
assert isinstance(dynamic_field, graphene.Dynamic)
505540
graphene_type = dynamic_field.get_type()
506541
assert isinstance(graphene_type, graphene.Field)

graphene_sqlalchemy/tests/test_types.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,8 +331,10 @@ class Meta:
331331

332332
pets_field = ReporterType._meta.fields["pets"]
333333
assert isinstance(pets_field, Dynamic)
334-
assert isinstance(pets_field.type().type, List)
335-
assert pets_field.type().type.of_type == PetType
334+
assert isinstance(pets_field.type().type, NonNull)
335+
assert isinstance(pets_field.type().type.of_type, List)
336+
assert isinstance(pets_field.type().type.of_type.of_type, NonNull)
337+
assert pets_field.type().type.of_type.of_type.of_type == PetType
336338
assert pets_field.type().description == "Overridden"
337339

338340

0 commit comments

Comments
 (0)