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

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7b568a..87cfb29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
 
 ## 0.13.0 (unreleased)
 
-- Updated GraphQL-core requirement to 3.1.0.
+- Updated GraphQL-core requirement to 3.1.3.
 
 
 ## 0.12.0 (2020-08-04)
diff --git a/ariadne/asgi.py b/ariadne/asgi.py
index f97cfaa..2402e74 100644
--- a/ariadne/asgi.py
+++ b/ariadne/asgi.py
@@ -26,7 +26,13 @@ from .file_uploads import combine_multipart_data
 from .format_error import format_error
 from .graphql import graphql, subscribe
 from .logger import log_error
-from .types import ContextValue, ErrorFormatter, ExtensionList, RootValue, ValidationRules
+from .types import (
+    ContextValue,
+    ErrorFormatter,
+    ExtensionList,
+    RootValue,
+    ValidationRules,
+)
 
 GQL_CONNECTION_INIT = "connection_init"  # Client -> Server
 GQL_CONNECTION_ACK = "connection_ack"  # Server -> Client
diff --git a/ariadne/contrib/federation/utils.py b/ariadne/contrib/federation/utils.py
index 14acb1c..be7c5a8 100644
--- a/ariadne/contrib/federation/utils.py
+++ b/ariadne/contrib/federation/utils.py
@@ -115,7 +115,7 @@ def includes_directive(
         return False
 
     directives = gather_directives(type_object)
-    return any([d.name.value == directive_name for d in directives])
+    return any(d.name.value == directive_name for d in directives)
 
 
 def gather_directives(
diff --git a/ariadne/schema_visitor.py b/ariadne/schema_visitor.py
index b4aabe1..9ac24f9 100644
--- a/ariadne/schema_visitor.py
+++ b/ariadne/schema_visitor.py
@@ -563,7 +563,9 @@ def heal_schema(schema: GraphQLSchema) -> GraphQLSchema:
 
         each(type_.fields, _heal_field)
 
-    def heal_type(type_: GraphQLNamedType) -> GraphQLNamedType:
+    def heal_type(
+        type_: Union[GraphQLList, GraphQLNamedType, GraphQLNonNull]
+    ) -> Union[GraphQLList, GraphQLNamedType, GraphQLNonNull]:
         # Unwrap the two known wrapper types
         if isinstance(type_, GraphQLList):
             type_ = GraphQLList(heal_type(type_.of_type))
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
diff --git a/requirements.txt b/requirements.txt
index 3f9302c..9ddc28a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,6 @@
 #
 #    pip-compile --output-file=requirements.txt setup.py
 #
-
-graphql-core==3.1.1       # via ariadne (setup.py)
-starlette==0.13.2         # via ariadne (setup.py)
-typing-extensions==3.7.4.1  # via ariadne (setup.py)
+graphql-core==3.1.3
+starlette==0.14.2
+typing-extensions==3.7.4.3


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