Repository: graphql-python/graphene
Pull request #1504 (2023-05-04 21:05:08)

types: add option for strict connection types
This PR adds a `strict_types` meta option to the relay `Connection` class to simplify the use case identified in this issue: https://github.com/graphql-python/graphene-django/issues/901

Default of `False` is the old behavior, and when set to `True` generated `TConnection` types will be typed as `edges: [TEdge!]!` instead of `edges: [TEdge]!` and `TEdge` will have `node: T!` instead of `node: T`.

This avoids duplicating logic and string descriptions for subclasses which just want to implement this common behavior. If this behavior is deemed desirable by default it would be trivial to flip the flag's default or remove it entirely. It only make the types stricter so it oughtn't break any existing code (though it will cause unexpected changes to user's schema's nonetheless, which is why I kept the default to `False`).

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/graphql-python/graphene/pull/1504?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) Report
Patch coverage: **`100.00`**% and no project coverage change.
> Comparison is base [(`57cbef6`)](https://app.codecov.io/gh/graphql-python/graphene/commit/57cbef6666e2e466808cce21b8a1769ecd3fd118?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) 96.00% compared to head [(`3943353`)](https://app.codecov.io/gh/graphql-python/graphene/pull/1504?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) 96.00%.

> :exclamation: Current head 3943353 differs from pull request most recent head 692e985. Consider uploading reports for the commit 692e985 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #1504   +/-   ##
=======================================
  Coverage   96.00%   96.00%           
=======================================
  Files          51       51           
  Lines        1750     1750           
=======================================
  Hits         1680     1680           
  Misses         70       70           
```


| [Impacted Files](https://app.codecov.io/gh/graphql-python/graphene/pull/1504?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python) | Coverage Δ | |
|---|---|---|
| [graphene/relay/connection.py](https://app.codecov.io/gh/graphql-python/graphene/pull/1504?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python#diff-Z3JhcGhlbmUvcmVsYXkvY29ubmVjdGlvbi5weQ==) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/graphql-python/graphene/pull/1504?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=graphql-python).

shrouxm: @erikwrede does that look like what you meant?
shrouxm: @erikwrede it looks like the checks will never complete because the python 3.6 test workflow no longer exists after https://github.com/graphql-python/graphene/pull/1507/files but the GitHub repo settings still require that workflow to run

Patch:
diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py
index ea49736..cc7d2da 100644
--- a/graphene/relay/connection.py
+++ b/graphene/relay/connection.py
@@ -13,12 +13,18 @@ from .node import is_node, AbstractNode
 
 
 def get_edge_class(
-    connection_class: Type["Connection"], _node: Type[AbstractNode], base_name: str
+    connection_class: Type["Connection"],
+    _node: Type[AbstractNode],
+    base_name: str,
+    strict_types: bool = False,
 ):
     edge_class = getattr(connection_class, "Edge", None)
 
     class EdgeBase:
-        node = Field(_node, description="The item at the end of the edge")
+        node = Field(
+            NonNull(_node) if strict_types else _node,
+            description="The item at the end of the edge",
+        )
         cursor = String(required=True, description="A cursor for use in pagination")
 
     class EdgeMeta:
@@ -83,7 +89,9 @@ class Connection(ObjectType):
         abstract = True
 
     @classmethod
-    def __init_subclass_with_meta__(cls, node=None, name=None, _meta=None, **options):
+    def __init_subclass_with_meta__(
+        cls, node=None, name=None, strict_types=False, _meta=None, **options
+    ):
         if not _meta:
             _meta = ConnectionOptions(cls)
         assert node, f"You have to provide a node in {cls.__name__}.Meta"
@@ -111,10 +119,10 @@ class Connection(ObjectType):
             )
 
         if "edges" not in _meta.fields:
-            edge_class = get_edge_class(cls, node, base_name)  # type: ignore
+            edge_class = get_edge_class(cls, node, base_name, strict_types)  # type: ignore
             cls.Edge = edge_class
             _meta.fields["edges"] = Field(
-                NonNull(List(edge_class)),
+                NonNull(List(NonNull(edge_class) if strict_types else edge_class)),
                 description="Contains the nodes in this connection.",
             )
 


Test patch:
diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py
index d45eea9..9c8b89d 100644
--- a/graphene/relay/tests/test_connection.py
+++ b/graphene/relay/tests/test_connection.py
@@ -299,3 +299,20 @@ def test_connectionfield_required():
     executed = schema.execute("{ testConnection { edges { cursor } } }")
     assert not executed.errors
     assert executed.data == {"testConnection": {"edges": []}}
+
+
+def test_connectionfield_strict_types():
+    class MyObjectConnection(Connection):
+        class Meta:
+            node = MyObject
+            strict_types = True
+
+    connection_field = ConnectionField(MyObjectConnection)
+    edges_field_type = connection_field.type._meta.fields["edges"].type
+    assert isinstance(edges_field_type, NonNull)
+
+    edges_list_element_type = edges_field_type.of_type.of_type
+    assert isinstance(edges_list_element_type, NonNull)
+
+    node_field = edges_list_element_type.of_type._meta.fields["node"]
+    assert isinstance(node_field.type, NonNull)