Repository: mirumee/ariadne

Issue:
snake_case_fallback_resolvers not calling obj.get(attr_name)
**Ariadne version:** 0.13.0
**Python version:** 3.8.11

Hello. I am using the [databases](https://www.encode.io/databases/) package with an [asyncpg](https://magicstack.github.io/asyncpg/current/) backend to interact with a PostgreSQL database. The objects returned from my queries are of the type `databases.backends.postgres.Record`.  The desired attributes can only can accessed via the get method. However, when I use `snake_case_fallback_resolvers`, Ariadne has trouble resolving the requested fields and I receive the following error: `Cannot return null for non-nullable field`

If I instead use the regular `fallback_resolvers` (adjusting my schema's naming conventions), Ariadne is able to resolve the requested fields.

Is this a bug or am I doing something wrong? Thank you for your time.


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14971ee..d3898fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,8 @@
 - Updated Starlette dependency to 0.15.
 - Added support for multiple keys for GraphQL federations.
 - Made `Query` type optional in federated schemas.
-
+- Updated default resolvers to test for `Mapping` instead of `dict`.
+ 
 ## 0.13.0 (2021-03-17)
 
 - Updated `graphQL-core` requirement to 3.1.3.
diff --git a/ariadne/resolvers.py b/ariadne/resolvers.py
index 9e9bf80..e01bdbc 100644
--- a/ariadne/resolvers.py
+++ b/ariadne/resolvers.py
@@ -1,3 +1,4 @@
+from collections.abc import Mapping
 from typing import Any
 
 from graphql import default_field_resolver
@@ -41,7 +42,7 @@ snake_case_fallback_resolvers = SnakeCaseFallbackResolversSetter()
 
 
 def resolve_parent_field(parent: Any, field_name: str) -> Any:
-    if isinstance(parent, dict):
+    if isinstance(parent, Mapping):
         return parent.get(field_name)
     return getattr(parent, field_name, None)
 
diff --git a/ariadne/utils.py b/ariadne/utils.py
index 2a3e3bf..e855588 100644
--- a/ariadne/utils.py
+++ b/ariadne/utils.py
@@ -1,4 +1,5 @@
 import asyncio
+from collections.abc import Mapping
 from functools import wraps
 from typing import Optional, Union, Callable, Dict, Any
 
@@ -49,13 +50,15 @@ def unwrap_graphql_error(
 
 
 def convert_kwargs_to_snake_case(func: Callable) -> Callable:
-    def convert_to_snake_case(d: Dict) -> Dict:
+    def convert_to_snake_case(m: Mapping) -> Dict:
         converted: Dict = {}
-        for k, v in d.items():
-            if isinstance(v, dict):
+        for k, v in m.items():
+            if isinstance(v, Mapping):
                 v = convert_to_snake_case(v)
             if isinstance(v, list):
-                v = [convert_to_snake_case(i) if isinstance(i, dict) else i for i in v]
+                v = [
+                    convert_to_snake_case(i) if isinstance(i, Mapping) else i for i in v
+                ]
             converted[convert_camel_case_to_snake(k)] = v
         return converted
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index eef6627..b545a3d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,5 @@
+from collections.abc import Mapping
+
 import pytest
 from graphql.validation.rules import ValidationRule
 
@@ -123,3 +125,21 @@ def validation_rule():
         pass
 
     return NoopRule
+
+
+@pytest.fixture
+def fake_mapping():
+    class FakeMapping(Mapping):
+        def __init__(self, **kwargs):
+            self._dummy = {**kwargs}
+
+        def __getitem__(self, key):
+            return self._dummy[key]
+
+        def __iter__(self):
+            return iter(self._dummy)
+
+        def __len__(self):
+            return len(self._dummy.keys())
+
+    return FakeMapping
diff --git a/tests/test_kwargs_camel_case_conversion.py b/tests/test_kwargs_camel_case_conversion.py
index f1812e1..4a37103 100644
--- a/tests/test_kwargs_camel_case_conversion.py
+++ b/tests/test_kwargs_camel_case_conversion.py
@@ -5,30 +5,46 @@ from ariadne import convert_kwargs_to_snake_case
 
 def test_decorator_converts_kwargs_to_camel_case():
     @convert_kwargs_to_snake_case
-    def my_func(*_, **kwargs):
+    def wrapped_func(*_, **kwargs):
         assert kwargs == {
             "first_parameter": True,
             "second_parameter": "value",
             "nested_parameter": {"first_sub_entry": 1, "second_sub_entry": 2},
         }
 
-    my_func(
+    wrapped_func(
         firstParameter=True,
         secondParameter="value",
         nestedParameter={"firstSubEntry": 1, "secondSubEntry": 2},
     )
 
 
+def test_decorator_converts_kwargs_to_camel_case_for_mapping(fake_mapping):
+    @convert_kwargs_to_snake_case
+    def wrapped_func(*_, **kwargs):
+        assert kwargs == {
+            "first_parameter": True,
+            "second_parameter": "value",
+            "nested_parameter": {"first_sub_entry": 1, "second_sub_entry": 2},
+        }
+
+    wrapped_func(
+        firstParameter=True,
+        secondParameter="value",
+        nestedParameter=fake_mapping(firstSubEntry=1, secondSubEntry=2),
+    )
+
+
 def test_decorator_leaves_snake_case_kwargs_unchanged():
     @convert_kwargs_to_snake_case
-    def my_func(*_, **kwargs):
+    def wrapped_func(*_, **kwargs):
         assert kwargs == {
             "first_parameter": True,
             "second_parameter": "value",
             "nested_parameter": {"first_sub_entry": 1, "second_sub_entry": 2},
         }
 
-    my_func(
+    wrapped_func(
         first_parameter=True,
         second_parameter="value",
         nested_parameter={"first_sub_entry": 1, "second_sub_entry": 2},
@@ -37,7 +53,7 @@ def test_decorator_leaves_snake_case_kwargs_unchanged():
 
 def test_decorator_converts_objects_in_lists_to_camel_case():
     @convert_kwargs_to_snake_case
-    def my_func(*_, **kwargs):
+    def wrapped_func(*_, **kwargs):
         assert kwargs == {
             "first_parameter": True,
             "list_of_items": [
@@ -45,21 +61,37 @@ def test_decorator_converts_objects_in_lists_to_camel_case():
             ],
         }
 
-    my_func(
+    wrapped_func(
         firstParameter=True,
         listOfItems=[{"firstProperty": 1, "secondProperty": 2}],
     )
 
 
+def test_decorator_converts_mappings_in_lists_to_camel_case(fake_mapping):
+    @convert_kwargs_to_snake_case
+    def wrapped_func(*_, **kwargs):
+        assert kwargs == {
+            "first_parameter": True,
+            "list_of_items": [
+                {"first_property": 1, "second_property": 2},
+            ],
+        }
+
+    wrapped_func(
+        firstParameter=True,
+        listOfItems=[fake_mapping(firstProperty=1, secondProperty=2)],
+    )
+
+
 def test_decorator_leaves_primitives_in_lists_unchanged():
     @convert_kwargs_to_snake_case
-    def my_func(*_, **kwargs):
+    def wrapped_func(*_, **kwargs):
         assert kwargs == {
             "first_parameter": True,
             "list_of_items": ["firstItem", "secondItem"],
         }
 
-    my_func(
+    wrapped_func(
         firstParameter=True,
         listOfItems=["firstItem", "secondItem"],
     )
@@ -68,14 +100,14 @@ def test_decorator_leaves_primitives_in_lists_unchanged():
 @pytest.mark.asyncio
 async def test_decorator_converts_kwargs_to_camel_case_for_async_resolver():
     @convert_kwargs_to_snake_case
-    async def my_func(*_, **kwargs):
+    async def wrapped_func(*_, **kwargs):
         assert kwargs == {
             "first_parameter": True,
             "second_parameter": "value",
             "nested_parameter": {"first_sub_entry": 1, "second_sub_entry": 2},
         }
 
-    await my_func(
+    await wrapped_func(
         firstParameter=True,
         secondParameter="value",
         nestedParameter={"firstSubEntry": 1, "secondSubEntry": 2},
@@ -85,14 +117,14 @@ async def test_decorator_converts_kwargs_to_camel_case_for_async_resolver():
 @pytest.mark.asyncio
 async def test_decorator_leaves_snake_case_kwargs_unchanged_for_async_resolver():
     @convert_kwargs_to_snake_case
-    async def my_func(*_, **kwargs):
+    async def wrapped_func(*_, **kwargs):
         assert kwargs == {
             "first_parameter": True,
             "second_parameter": "value",
             "nested_parameter": {"first_sub_entry": 1, "second_sub_entry": 2},
         }
 
-    await my_func(
+    await wrapped_func(
         first_parameter=True,
         second_parameter="value",
         nested_parameter={"first_sub_entry": 1, "second_sub_entry": 2},