Repository: litestar-org/litestar

Issue:
Partial doesn't work with inherited fields
```python
from starlite import Partial, get
from pydantic import BaseModel

class Parent(BaseModel):
    foo: int

class Child(Parent):
    bar: int

@get("/test")
def example(obj: Partial[Child]) -> None:
    print(obj)
```

In the above example, `Partial[Child]` only accepts the field `bar: Optional[int]` and ignores all fields from the superclass. I couldn't find this behaviour documented anywhere so I assume this isn't intended?

```python
Python 3.10.5 (main, Jun 23 2022, 17:14:57) [Clang 13.1.6 (clang-1316.0.21.2.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from starlite import Partial
>>> from pydantic import BaseModel
>>> class Parent(BaseModel):
...     foo: int
...
>>> class Child(Parent):
...     bar: int
...
>>> PartialChild = Partial[Child]
>>> PartialChild.__annotations__
{'bar': typing.Optional[int]}
>>>
```

This behaviour can also be seen above



Discussion:
I believe the following implementation would work:

```python
class Partial(Generic[T]):
    _models: Dict[Type[T], Any] = {}

    def __class_getitem__(cls, item: Type[T]) -> Type[T]:
        """
        Modifies a given T subclass of BaseModel to be all optional
        """
        if not cls._models.get(item):
            field_definitions: Dict[str, Tuple[Any, None]] = {}
            for obj in item.mro():
                if issubclass(obj, BaseModel):
                    for field_name, field_type in obj.__annotations__.items():
                        # we modify the field annotations to make it optional
                        if not isinstance(field_type, GenericAlias) or type(None) not in field_type.__args__:
                            field_definitions[field_name] = (Optional[field_type], None)
                        else:
                            field_definitions[field_name] = (field_type, None)
                else:
                    break
            cls._models[item] = create_model(f"Partial{item.__name__}", **field_definitions)  # type: ignore
        return cast(Type[T], cls._models.get(item))
```

This implementation will traverse the class' mro and grab all annotations from parent classes until `BaseModel` is reached.

This does, however, change the runtime behaviour of `Partial` since even though the linter complains, you can currently pass a non BaseModel class into Partial:

```python
>>> from starlite import Partial
>>> class Parent:
...     foo: int
...     bar: int
...
>>> temp = Partial[Parent]
>>> temp.__annotations__
{'foo': typing.Optional[int], 'bar': typing.Optional[int]}
```

Whereas, after this change the same code would simply produce `{}` as `Parent` is not a subclass of `pydantic.BaseModel`.

Currently the documentation for this class says "Modifies a given T subclass of BaseModel to be all optional" so the new behaviour would be correct from that perspective.
Great! Would you like to add a PR? 

Resolving patch:
diff --git a/starlite/types.py b/starlite/types.py
--- a/starlite/types.py
+++ b/starlite/types.py
@@ -11,6 +11,7 @@
     TypeVar,
     Union,
     cast,
+    get_type_hints,
 )
 
 from openapi_schema_pydantic.v3.v3_1_0.header import Header
@@ -23,7 +24,7 @@
 from starlette.responses import Response as StarletteResponse
 from typing_extensions import Literal, Protocol, runtime_checkable
 
-from starlite.exceptions import HTTPException
+from starlite.exceptions import HTTPException, ImproperlyConfiguredException
 from starlite.response import Response
 
 try:
@@ -103,15 +104,23 @@ def __class_getitem__(cls, item: Type[T]) -> Type[T]:
         """
         Modifies a given T subclass of BaseModel to be all optional
         """
+        if not issubclass(item, BaseModel):
+            raise ImproperlyConfiguredException(f"Partial[{item}] must be a subclass of BaseModel")
         if not cls._models.get(item):
             field_definitions: Dict[str, Tuple[Any, None]] = {}
-            for field_name, field_type in item.__annotations__.items():
-                # we modify the field annotations to make it optional
-                if not isinstance(field_type, GenericAlias) or type(None) not in field_type.__args__:
-                    field_definitions[field_name] = (Optional[field_type], None)
+            # traverse the object's mro and get all annotations
+            # until we find a BaseModel.
+            for obj in item.mro():
+                if issubclass(obj, BaseModel):
+                    for field_name, field_type in get_type_hints(obj).items():
+                        # we modify the field annotations to make it optional
+                        if not isinstance(field_type, GenericAlias) or type(None) not in field_type.__args__:
+                            field_definitions[field_name] = (Optional[field_type], None)
+                        else:
+                            field_definitions[field_name] = (field_type, None)
                 else:
-                    field_definitions[field_name] = (field_type, None)
-                cls._models[item] = create_model("Partial" + item.__name__, **field_definitions)  # type: ignore
+                    break
+            cls._models[item] = create_model(f"Partial{item.__name__}", **field_definitions)  # type: ignore
         return cast(Type[T], cls._models.get(item))
 
 


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -1,3 +1,9 @@
+from typing import Optional
+
+from pydantic import BaseModel
+from pytest import raises
+
+from starlite.exceptions import ImproperlyConfiguredException
 from starlite.types import Partial
 from tests import Person
 
@@ -16,3 +22,42 @@ def test_partial() -> None:
     for field in partial.__annotations__.values():
         assert isinstance(field, GenericAlias)
         assert type(None) in field.__args__
+
+
+def test_partial_superclass() -> None:
+    """
+    Test that Partial returns the correct annotations
+    for nested models.
+    """
+
+    class Parent(BaseModel):
+        foo: int
+
+    class Child(Parent):
+        bar: int
+
+    partial_child = Partial[Child]
+
+    for field in partial_child.__fields__.values():  # type: ignore
+        assert field.allow_none
+        assert not field.required
+    assert partial_child.__annotations__ == {
+        "foo": Optional[int],
+        "bar": Optional[int],
+    }
+
+
+def test_partial_basemodel() -> None:
+    """
+    Test that Partial returns no annotations for classes
+    that don't inherit from BaseModel.
+    """
+
+    class Foo:
+        bar: int
+
+    # The type checker will raise a warning that Foo is not a BaseModel
+    # but we want to test for runtime behaviour in case someone passes in
+    # a class that doesn't inherit from BaseModel anyway.
+    with raises(ImproperlyConfiguredException):
+        Partial[Foo]  # type: ignore