Repository: strawberry-graphql/strawberry

Issue:
LazyType inside Union throws 'LazyType' object has no attribute '__name__'
Using LazyTypes inside a Union throws an error, e.g:
```
@strawberry.type
class SelectedTpmType:
  tpm: Union[strawberry.LazyType["VideoType", ".video"], strawberry.LazyType["PressType", ".press"]]
```

Throws:
```
venv\lib\site-packages\strawberry\schema\schema.py", line 64, in init
    self._schema = GraphQLSchema(
  venv\lib\site-packages\graphql\type\schema.py", line 208, in init
    collect_referenced_types(query)
  venv\lib\site-packages\graphql\type\schema.py", line 423, in collect_referenced_types
    collect_referenced_types(field.type)
  venv\lib\site-packages\graphql\type\schema.py", line 423, in collect_referenced_types
    collect_referenced_types(field.type)
  venv\lib\site-packages\graphql\type\schema.py", line 422, in collect_referenced_types
    for field in named_type.fields.values():
  File "C:\Python38\lib\functools.py", line 967, in get
    val = self.func(instance)
  venv\lib\site-packages\graphql\type\definition.py", line 769, in fields
    raise TypeError(f"{self.name} fields cannot be resolved. {error}")
TypeError: SelectedTpmType fields cannot be resolved. 'LazyType' object has no attribute '__name__'
```


while `tpm: strawberry.LazyType["PressType", ".press"]` and `tpm: strawberry.LazyType["VideoType", ".video"]`

works without any exceptions

I did not test if this is a LazyType _only_ problem or a general problem with LazyTypes inside Union (e.g. mixing LazyTypes with "normal" Types)


Discussion:
Is it not possible to use List type inside union? I mean the following one

```python
CollectionCreateResponse = strawberry.union('CollectionCreatePayload', [Collection, List[Error]])
```
This throws similar issue of AttributeError.

This works though


```python
CollectionCreateResponse = strawberry.union('CollectionCreatePayload', [Collection, Error])
```

Sorry, didn't want to create a separate issue so I am commenting here as it is related to Union type. It might be my lack of knowledge on it. 
This issue is related to #1104 and #1158

Resolving patch:
diff --git a/strawberry/schema/name_converter.py b/strawberry/schema/name_converter.py
--- a/strawberry/schema/name_converter.py
+++ b/strawberry/schema/name_converter.py
@@ -29,6 +29,12 @@ class NameConverter:
     def __init__(self, auto_camel_case: bool = True) -> None:
         self.auto_camel_case = auto_camel_case
 
+    def apply_naming_config(self, name: str) -> str:
+        if self.auto_camel_case:
+            name = to_camel_case(name)
+
+        return name
+
     def from_type(
         self,
         type_: Union[StrawberryType, StrawberryDirective, StrawberryDirective],
@@ -47,8 +53,8 @@ def from_type(
             return self.from_union(type_)
         elif isinstance(type_, ScalarDefinition):  # TODO: Replace with StrawberryScalar
             return self.from_scalar(type_)
-
-        raise TypeError(f"Unexpected type '{type_}'")
+        else:
+            return str(type_)
 
     def from_argument(self, argument: StrawberryArgument) -> str:
         return self.get_graphql_name(argument)
@@ -97,8 +103,14 @@ def from_union(self, union: StrawberryUnion) -> str:
             if isinstance(type_, LazyType):
                 type_ = cast("StrawberryType", type_.resolve_type())
 
-            assert hasattr(type_, "_type_definition")
-            name += self.from_type(type_._type_definition)
+            if hasattr(type_, "_type_definition"):
+                type_name = self.from_type(type_._type_definition)
+            else:
+                # This should only be hit when generating names for type-related
+                # exceptions
+                type_name = self.from_type(type_)
+
+            name += type_name
 
         return name
 
@@ -166,7 +178,4 @@ def get_graphql_name(self, obj: HasGraphQLName) -> str:
 
         assert obj.python_name
 
-        if self.auto_camel_case:
-            return to_camel_case(obj.python_name)
-
-        return obj.python_name
+        return self.apply_naming_config(obj.python_name)
diff --git a/strawberry/schema/schema_converter.py b/strawberry/schema/schema_converter.py
--- a/strawberry/schema/schema_converter.py
+++ b/strawberry/schema/schema_converter.py
@@ -40,6 +40,7 @@
 from strawberry.exceptions import (
     DuplicatedTypeName,
     InvalidTypeInputForUnion,
+    InvalidUnionTypeError,
     MissingTypesForGenericError,
     ScalarAlreadyRegisteredError,
     UnresolvedFieldTypeError,
@@ -631,6 +632,13 @@ def from_type(self, type_: Union[StrawberryType, type]) -> GraphQLNullableType:
     def from_union(self, union: StrawberryUnion) -> GraphQLUnionType:
         union_name = self.config.name_converter.from_type(union)
 
+        for type_ in union.types:
+            # This check also occurs in the Annotation resolving, but because of
+            # TypeVars, Annotations, LazyTypes, etc it can't perfectly detect issues at
+            # that stage
+            if not StrawberryUnion.is_valid_union_type(type_):
+                raise InvalidUnionTypeError(union_name, type_)
+
         # Don't reevaluate known types
         if union_name in self.type_map:
             graphql_union = self.type_map[union_name].implementation
diff --git a/strawberry/union.py b/strawberry/union.py
--- a/strawberry/union.py
+++ b/strawberry/union.py
@@ -17,6 +17,7 @@
     Union,
     cast,
 )
+from typing_extensions import Annotated, get_origin
 
 from graphql import GraphQLNamedType, GraphQLUnionType
 
@@ -109,7 +110,16 @@ def _get_type_params(type_: StrawberryType):
 
     @property
     def is_generic(self) -> bool:
-        return len(self.type_params) > 0
+        def _is_generic(type_: object) -> bool:
+            if hasattr(type_, "_type_definition"):
+                type_ = type_._type_definition
+
+            if isinstance(type_, StrawberryType):
+                return type_.is_generic
+
+            return False
+
+        return any(map(_is_generic, self.types))
 
     def copy_with(
         self, type_var_map: Mapping[TypeVar, Union[StrawberryType, type]]
@@ -202,6 +212,22 @@ def _resolve_union_type(
 
         return _resolve_union_type
 
+    @staticmethod
+    def is_valid_union_type(type_: object) -> bool:
+        # Usual case: Union made of @strawberry.types
+        if hasattr(type_, "_type_definition"):
+            return True
+
+        # Can't confidently assert that these types are valid/invalid within Unions
+        # until full type resolving stage is complete
+        ignored_types = (LazyType, TypeVar)
+        if isinstance(type_, ignored_types):
+            return True
+        if get_origin(type_) is Annotated:
+            return True
+
+        return False
+
 
 Types = TypeVar("Types", bound=Type)
 
@@ -234,7 +260,9 @@ def union(
         raise TypeError("No types passed to `union`")
 
     for type_ in types:
-        if not isinstance(type_, TypeVar) and not hasattr(type_, "_type_definition"):
+        # Due to TypeVars, Annotations, LazyTypes, etc., this does not perfectly detect
+        # issues. This check also occurs in the Schema conversion stage as a backup.
+        if not StrawberryUnion.is_valid_union_type(type_):
             raise InvalidUnionTypeError(union_name=name, invalid_type=type_)
 
     union_definition = StrawberryUnion(


Test patch:
diff --git a/tests/schema/test_union.py b/tests/schema/test_union.py
--- a/tests/schema/test_union.py
+++ b/tests/schema/test_union.py
@@ -7,6 +7,7 @@
 import pytest
 
 import strawberry
+from strawberry.exceptions import InvalidUnionTypeError
 from strawberry.lazy_type import lazy
 
 
@@ -644,3 +645,20 @@ def b(self) -> Union[TypeA, TypeB]:
 
     assert result.data["a"]["__typename"] == "TypeA"
     assert result.data["b"]["__typename"] == "TypeB"
+
+
+@pytest.mark.raises_strawberry_exception(
+    InvalidUnionTypeError, match="Type `int` cannot be used in a GraphQL Union"
+)
+def test_error_with_invalid_annotated_type():
+    @strawberry.type
+    class Something:
+        h: str
+
+    AnnotatedInt = Annotated[int, "something_else"]
+
+    @strawberry.type
+    class Query:
+        union: Union[Something, AnnotatedInt]
+
+    strawberry.Schema(query=Query)
diff --git a/tests/types/test_lazy_types.py b/tests/types/test_lazy_types.py
--- a/tests/types/test_lazy_types.py
+++ b/tests/types/test_lazy_types.py
@@ -1,6 +1,7 @@
 # type: ignore
 import enum
 from typing import Generic, TypeVar
+from typing_extensions import Annotated
 
 import strawberry
 from strawberry.annotation import StrawberryAnnotation
@@ -8,6 +9,7 @@
 from strawberry.lazy_type import LazyType
 from strawberry.types.fields.resolver import StrawberryResolver
 from strawberry.types.types import TypeDefinition
+from strawberry.union import StrawberryUnion, union
 
 
 # This type is in the same file but should adequately test the logic.
@@ -36,6 +38,16 @@ def test_lazy_type():
     assert resolved.resolve_type() is LaziestType
 
 
+def test_lazy_type_function():
+    LethargicType = Annotated["LaziestType", strawberry.lazy("test_lazy_types")]
+
+    annotation = StrawberryAnnotation(LethargicType)
+    resolved = annotation.resolve()
+
+    assert isinstance(resolved, LazyType)
+    assert resolved.resolve_type() is LaziestType
+
+
 def test_lazy_type_enum():
     # Module path is short and relative because of the way pytest runs the file
     LazierType = LazyType("LazyEnum", "test_lazy_types")
@@ -129,3 +141,35 @@ def slaking_pokemon() -> LazierType:
     assert isinstance(resolver.type, LazyType)
     assert resolver.type is LazierType
     assert resolver.type.resolve_type() is LaziestType
+
+
+def test_lazy_type_in_union():
+    ActiveType = LazyType("LaziestType", "test_lazy_types")
+    ActiveEnum = LazyType("LazyEnum", "test_lazy_types")
+
+    something = union(name="CoolUnion", types=(ActiveType, ActiveEnum))
+    annotation = StrawberryAnnotation(something)
+
+    resolved = annotation.resolve()
+    assert isinstance(resolved, StrawberryUnion)
+
+    [type1, type2] = resolved.types
+    assert type1 is ActiveType
+    assert type2 is ActiveEnum
+    assert type1.resolve_type() is LaziestType
+    assert type2.resolve_type() is LazyEnum
+
+
+def test_lazy_function_in_union():
+    ActiveType = Annotated["LaziestType", strawberry.lazy("test_lazy_types")]
+    ActiveEnum = Annotated["LazyEnum", strawberry.lazy("test_lazy_types")]
+
+    something = union(name="CoolUnion", types=(ActiveType, ActiveEnum))
+    annotation = StrawberryAnnotation(something)
+
+    resolved = annotation.resolve()
+    assert isinstance(resolved, StrawberryUnion)
+
+    [type1, type2] = resolved.types
+    assert type1.resolve_type() is LaziestType
+    assert type2.resolve_type() is LazyEnum