Repository: mirumee/ariadne

Issue:
Support passing Python `Enum` directly to `make_executable_schema`
`make_executable_schema` could have two lines of magic that would repack `Enum`s to `ariadne.EnumType(enum.__name__, enum).bind_to_schema(schema)`


Discussion:

Resolving patch:
diff --git a/ariadne/contrib/federation/schema.py b/ariadne/contrib/federation/schema.py
--- a/ariadne/contrib/federation/schema.py
+++ b/ariadne/contrib/federation/schema.py
@@ -10,9 +10,12 @@
     GraphQLUnionType,
 )
 
-from ...executable_schema import make_executable_schema, join_type_defs
+from ...executable_schema import (
+    SchemaBindables,
+    make_executable_schema,
+    join_type_defs,
+)
 from ...schema_visitor import SchemaDirectiveVisitor
-from ...types import SchemaBindable
 from .utils import get_entity_types, purge_schema_directives, resolve_entities
 
 base_federation_service_type_defs = """
@@ -70,7 +73,7 @@ def has_query_type(type_defs: str) -> bool:
 
 def make_federated_schema(
     type_defs: Union[str, List[str]],
-    *bindables: Union[SchemaBindable, List[SchemaBindable]],
+    *bindables: SchemaBindables,
     directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None,
 ) -> GraphQLSchema:
     if isinstance(type_defs, list):
diff --git a/ariadne/executable_schema.py b/ariadne/executable_schema.py
--- a/ariadne/executable_schema.py
+++ b/ariadne/executable_schema.py
@@ -1,3 +1,4 @@
+from enum import Enum
 from typing import Dict, List, Optional, Type, Union
 
 from graphql import (
@@ -16,10 +17,14 @@
 from .schema_visitor import SchemaDirectiveVisitor
 from .types import SchemaBindable
 
+SchemaBindables = Union[
+    SchemaBindable, Type[Enum], List[Union[SchemaBindable, Type[Enum]]]
+]
+
 
 def make_executable_schema(
     type_defs: Union[str, List[str]],
-    *bindables: Union[SchemaBindable, List[SchemaBindable]],
+    *bindables: SchemaBindables,
     directives: Optional[Dict[str, Type[SchemaDirectiveVisitor]]] = None,
     convert_names_case: Union[bool, SchemaNameConverter] = False,
 ) -> GraphQLSchema:
@@ -87,13 +92,19 @@ def make_executable_schema(
 
     ```python
     from dataclasses import dataclass
+    from enum import Enum
     from ariadne import ObjectType, QueryType, UnionType, graphql_sync, make_executable_schema
 
     # Define some types representing database models in real applications
+    class UserLevel(str, Enum):
+        USER = "user"
+        ADMIN = "admin"
+
     @dataclass
     class UserModel:
         id: str
         name: str
+        level: UserLevel
 
     @dataclass
     class PostModel:
@@ -102,9 +113,9 @@ class PostModel:
 
     # Create fake "database"
     results = (
-        UserModel(id=1, name="Bob"),
-        UserModel(id=2, name="Alice"),
-        UserModel(id=3, name="Jon"),
+        UserModel(id=1, name="Bob", level=UserLevel.USER),
+        UserModel(id=2, name="Alice", level=UserLevel.ADMIN),
+        UserModel(id=3, name="Jon", level=UserLevel.USER),
         PostModel(id=1, body="Hello world!"),
         PostModel(id=2, body="How's going?"),
         PostModel(id=3, body="Sure thing!"),
@@ -155,12 +166,18 @@ def resolve_result_type(obj: UserModel | PostModel | dict, *_) -> str:
         type User {
             id: ID!
             username: String!
+            level: UserLevel!
         }
 
         type Post {
             id: ID!
             message: String!
         }
+
+        enum UserLevel {
+            USER
+            ADMIN
+        }
         \"\"\",
         # Bindables *args accept single instances:
         query_type,
@@ -168,6 +185,8 @@ def resolve_result_type(obj: UserModel | PostModel | dict, *_) -> str:
         # Bindables *args accepts lists of instances:
         [user_type, post_type],
         # Both approaches can be mixed
+        # Python Enums are also valid bindables:
+        UserLevel,
     )
 
     # Query the schema for results
@@ -185,6 +204,7 @@ def resolve_result_type(obj: UserModel | PostModel | dict, *_) -> str:
                         ... on User {
                             id
                             username
+                            level
                         }
                     }
                 }
@@ -201,14 +221,17 @@ def resolve_result_type(obj: UserModel | PostModel | dict, *_) -> str:
                 {
                     "id": "1",
                     "username": "Bob",
+                    "level": "USER",
                 },
                 {
                     "id": "2",
                     "username": "Alice",
+                    "level": "ADMIN",
                 },
                 {
                     "id": "3",
                     "username": "Jon",
+                    "level": "USER",
                 },
                 {
                     "id": "1",
@@ -314,10 +337,11 @@ def uppercase_resolved_value(*args, **kwargs):
 
     ast_document = parse(type_defs)
     schema = build_ast_schema(ast_document)
-    flat_bindables: List[SchemaBindable] = flatten_bindables(*bindables)
+    normalized_bindables = normalize_bindables(*bindables)
 
-    for bindable in flat_bindables:
-        bindable.bind_to_schema(schema)
+    for bindable in normalized_bindables:
+        if isinstance(bindable, SchemaBindable):
+            bindable.bind_to_schema(schema)
 
     set_default_enum_values_on_schema(schema)
 
@@ -326,7 +350,7 @@ def uppercase_resolved_value(*args, **kwargs):
 
     assert_valid_schema(schema)
     validate_schema_enum_values(schema)
-    repair_default_enum_values(schema, flat_bindables)
+    repair_default_enum_values(schema, normalized_bindables)
 
     if convert_names_case:
         convert_schema_names(
@@ -341,9 +365,22 @@ def join_type_defs(type_defs: List[str]) -> str:
     return "\n\n".join(t.strip() for t in type_defs)
 
 
+def normalize_bindables(*bindables: SchemaBindables) -> List[SchemaBindable]:
+    normal_bindables: List[SchemaBindable] = []
+    for bindable in flatten_bindables(*bindables):
+        if isinstance(bindable, SchemaBindable):
+            normal_bindables.append(bindable)
+        elif issubclass(bindable, Enum):
+            normal_bindables.append(EnumType(bindable.__name__, bindable))
+        else:
+            raise ValueError(f"Unknown type: {repr(bindable)}")
+
+    return normal_bindables
+
+
 def flatten_bindables(
-    *bindables: Union[SchemaBindable, List[SchemaBindable]]
-) -> List[SchemaBindable]:
+    *bindables: SchemaBindables,
+) -> List[Union[SchemaBindable, Type[Enum]]]:
     new_bindables = []
 
     for bindable in bindables:
@@ -355,7 +392,9 @@ def flatten_bindables(
     return new_bindables
 
 
-def repair_default_enum_values(schema, bindables) -> None:
+def repair_default_enum_values(
+    schema: GraphQLSchema, bindables: List[SchemaBindable]
+) -> None:
     for bindable in bindables:
         if isinstance(bindable, EnumType):
             bindable.bind_to_default_values(schema)


Test patch:
diff --git a/tests/test_enums.py b/tests/test_enums.py
--- a/tests/test_enums.py
+++ b/tests/test_enums.py
@@ -474,3 +474,44 @@ def resolve_test_enum(*_, r):
     assert types_map["User"]["fields"][0]["args"][0]["defaultValue"] == "USER"
     assert result_hello_query.data["hello"]
     assert result_hello_query.errors is None
+
+
+def test_python_enums_can_be_passed_directly_to_make_executable_schema():
+    class Episode(Enum):
+        NEWHOPE = "new-hope"
+        EMPIRE = "empire-strikes"
+        JEDI = "return-jedi"
+
+    query = QueryType()
+    query.set_field("testEnum", lambda *_: Episode.NEWHOPE)
+
+    schema = make_executable_schema([enum_definition, enum_field], query, Episode)
+    result = graphql_sync(schema, "{ testEnum }")
+    assert result.data["testEnum"] == "NEWHOPE"
+
+
+def test_python_str_enums_can_be_passed_directly_to_make_executable_schema():
+    class Episode(str, Enum):
+        NEWHOPE = "new-hope"
+        EMPIRE = "empire-strikes"
+        JEDI = "return-jedi"
+
+    query = QueryType()
+    query.set_field("testEnum", lambda *_: Episode.NEWHOPE)
+
+    schema = make_executable_schema([enum_definition, enum_field], query, Episode)
+    result = graphql_sync(schema, "{ testEnum }")
+    assert result.data["testEnum"] == "NEWHOPE"
+
+
+def test_error_is_raised_for_python_enum_with_name_not_in_schema():
+    class UnknownEnum(str, Enum):
+        NEWHOPE = "new-hope"
+        EMPIRE = "empire-strikes"
+        JEDI = "return-jedi"
+
+    query = QueryType()
+    query.set_field("testEnum", lambda *_: UnknownEnum.NEWHOPE)
+
+    with pytest.raises(ValueError):
+        make_executable_schema([enum_definition, enum_field], query, UnknownEnum)
diff --git a/tests_mypy/enums.py b/tests_mypy/enums.py
--- a/tests_mypy/enums.py
+++ b/tests_mypy/enums.py
@@ -1,6 +1,6 @@
 from enum import Enum, IntEnum
 
-from ariadne import EnumType
+from ariadne import EnumType, make_executable_schema
 
 
 class TestEnum(Enum):
@@ -16,3 +16,18 @@ class TestIntEnum(IntEnum):
 test_dict_enum_type = EnumType("TestDictEnum", {"FOO": "foo", "BAR": "bar"})
 test_enum_type = EnumType("TestEnum", TestEnum)
 test_int_enum_type = EnumType("TestIntEnumType", TestIntEnum)
+
+
+make_executable_schema(
+    """
+    type Query {
+        test: TestEnum!
+    }
+
+    enum TestEnum {
+        FOO
+        VAR
+    }
+    """,
+    TestEnum,
+)