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:
Gotcha with `Record` classes from databases is that they don't inherit from `dict`, but `Mapping` while Ariadne is testing for the former. We should change our `instance` checks in default resolvers to check for `Mapping` instead, which seems like it should resolve most of the issues you are reporting.

Seems like very small change that can improve the experience here, so I'll put it on our roadmap.

Thanks for bringing this up!

Resolving patch:
diff --git a/ariadne/resolvers.py b/ariadne/resolvers.py
--- 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 @@ def add_resolver_to_field(
 
 
 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
--- 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
--- 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 @@ class NoopRule(ValidationRule):
         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
--- a/tests/test_kwargs_camel_case_conversion.py
+++ b/tests/test_kwargs_camel_case_conversion.py
@@ -5,30 +5,46 @@
 
 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 my_func(*_, **kwargs):
 
 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 my_func(*_, **kwargs):
             ],
         }
 
-    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 my_func(*_, **kwargs):
 @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 my_func(*_, **kwargs):
 @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},