Repository: mirumee/ariadne

Issue:
Unexpected Snake Case for Acronyms
The snake case conversion of the `snake_case_fallback_resolvers` yields unexpected results for words with multiple uppercase letters in a row, e.g.
 - `getHTTPResponse` is converted to `get_h_t_t_p_response`, or
 - `externalID` is converted to "external_i_d`.  

These are unlikely names for python attributes and I would expect the resolver to look for `get_http_response` / `external_id` instead.

Possible implementations for the camel to snake case conversions are discussed here: https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case

Discussion:

Patch:
diff --git a/ariadne/utils.py b/ariadne/utils.py
index 13c34d0..2a3e3bf 100644
--- a/ariadne/utils.py
+++ b/ariadne/utils.py
@@ -6,20 +6,29 @@ from graphql import GraphQLError, parse
 
 
 def convert_camel_case_to_snake(graphql_name: str) -> str:
+    # pylint: disable=too-many-boolean-expressions
+    max_index = len(graphql_name) - 1
+    lowered_name = graphql_name.lower()
+
     python_name = ""
-    for i, c in enumerate(graphql_name.lower()):
-        if (
-            i > 0
-            and (
-                all(
-                    (
-                        c != graphql_name[i],
-                        graphql_name[i - 1] != "_",
-                        graphql_name[i - 1] == python_name[-1],
-                    )
-                )
+    for i, c in enumerate(lowered_name):
+        if i > 0 and (
+            # testWord -> test_word
+            (
+                c != graphql_name[i]
+                and graphql_name[i - 1] != "_"
+                and graphql_name[i - 1] == python_name[-1]
+            )
+            # TESTWord -> test_word
+            or (
+                i < max_index
+                and graphql_name[i] != lowered_name[i]
+                and graphql_name[i + 1] == lowered_name[i + 1]
             )
-            or all((c.isdigit(), graphql_name[i - 1].isdigit() is False))
+            # test134 -> test_134
+            or (c.isdigit() and not graphql_name[i - 1].isdigit())
+            # 134test -> 134_test
+            or (not c.isdigit() and graphql_name[i - 1].isdigit())
         ):
             python_name += "_"
         python_name += c


Test patch:
diff --git a/tests/test_case_convertion_util.py b/tests/test_camel_case_to_snake_case_convertion.py
similarity index 78%
rename from tests/test_case_convertion_util.py
rename to tests/test_camel_case_to_snake_case_convertion.py
index 3b27626..b55040d 100644
--- a/tests/test_case_convertion_util.py
+++ b/tests/test_camel_case_to_snake_case_convertion.py
@@ -1,3 +1,5 @@
+import pytest
+
 from ariadne import convert_camel_case_to_snake
 
 
@@ -41,8 +43,16 @@ def test_no_underscore_added_if_previous_character_is_an_underscore():
 
 
 def test_no_underscore_added_if_previous_character_is_uppercase():
-    assert convert_camel_case_to_snake("testWithUPPERPart") == "test_with_upperpart"
-
-
-def test_digits_are_treated_as_word():
-    assert convert_camel_case_to_snake("testWith365InIt") == "test_with_365_in_it"
+    assert convert_camel_case_to_snake("testWithUPPERPart") == "test_with_upper_part"
+
+
+@pytest.mark.parametrize(
+    ("test_str", "result"),
+    [
+        ("testWith365InIt", "test_with_365_in_it"),
+        ("365testWithInIt", "365_test_with_in_it"),
+        ("testWithInIt365", "test_with_in_it_365"),
+    ],
+)
+def test_digits_are_treated_as_word(test_str, result):
+    assert convert_camel_case_to_snake(test_str) == result