Repository: caikit/caikit (python)

Issue:
Conversion of dataobjects-with-inheritance to pydantic models is broken
## Describe the bug

```
    @dataobject
    class Base(DataObjectBase):
        foo: int
        bar: int

    @dataobject
    class Derived(Base):
        bar: str
        baz: str
```

If we create a `pydantic model` using our `._dataobject_to_pydantic(Derived)` function call, it omits `foo`

## Platform

Please provide details about the environment you are using, including the following:

- Interpreter version: 3.9
- Library version: 0.16.x



Pull request description:
🐛 Fix derived DM to pydantic conversion bug
<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/caikit/caikit/blob/main/CONTRIBUTING.md
2. If this PR closes another issue, add 'closes #<issue number>' somewhere in the PR summary. GitHub will automatically close that issue when this PR gets merged. Alternatively, adding 'refs #<issue number>' will not close the issue, but help provide the reviewer more context.-->

**What this PR does / why we need it**:

Fixes an issue when converting dataobject to pydantic models when there is inheritance involved.

fix #419 

**Special notes for your reviewer**:

> FYI - this has a consequence in all cases where we have derived DM objects. Instantiating any derived DM object will require that we satisfy ALL fields within the base DM object as well. (applies to `S3Path`)

#### Useful links:
- https://docs.python.org/3/library/typing.html#typing.get_type_hints
- https://peps.python.org/pep-0526/#runtime-effects-of-type-annotations
    
     > The recommended way of getting annotations at runtime is by using typing.get_type_hints function; as with all dunder attributes, any undocumented use of __annotations__ is subject to breakage without warning:

**If applicable**:
- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/caikit/runtime/http_server.py b/caikit/runtime/http_server.py
index 087661ad..34d9cf53 100644
--- a/caikit/runtime/http_server.py
+++ b/caikit/runtime/http_server.py
@@ -18,7 +18,17 @@ API based on the task definitions available at boot.
 """
 # Standard
 from functools import partial
-from typing import Any, Dict, Iterable, List, Optional, Type, Union, get_args
+from typing import (
+    Any,
+    Dict,
+    Iterable,
+    List,
+    Optional,
+    Type,
+    Union,
+    get_args,
+    get_type_hints,
+)
 import asyncio
 import enum
 import json
@@ -624,26 +634,32 @@ class RuntimeHTTPServer(RuntimeServerBase):
         """Make a pydantic model based on the given proto message by using the data
         model class annotations to mirror as a pydantic model
         """
-        # Use dicts for jsondict values
-        # this is needed for pydantic to have a handle on JsonDicts while
+        # define a local namespace for type hints to get type information from.
+        # This is needed for pydantic to have a handle on JsonDict and JsonDictValue while
         # creating its base model
-        # pylint: disable=unused-variable
-        JsonDict = dict
-        JsonDictValue = dict
+        localns = {"JsonDict": dict, "JsonDictValue": dict}
 
         if dm_class in PYDANTIC_TO_DM_MAPPING:
             return PYDANTIC_TO_DM_MAPPING[dm_class]
 
         annotations = {
             field_name: cls._get_pydantic_type(field_type)
-            for field_name, field_type in dm_class.__annotations__.items()
+            for field_name, field_type in get_type_hints(
+                dm_class, localns=localns
+            ).items()
         }
         pydantic_model = type(pydantic.BaseModel)(
             dm_class.get_proto_class().DESCRIPTOR.full_name,
             (pydantic.BaseModel,),
             {
                 "__annotations__": annotations,
-                **{name: None for name in dm_class.__annotations__},
+                **{
+                    name: None
+                    for name, _ in get_type_hints(
+                        dm_class,
+                        localns=localns,
+                    ).items()
+                },
             },
         )
         PYDANTIC_TO_DM_MAPPING[dm_class] = pydantic_model


Test patch:
diff --git a/tests/runtime/test_http_server.py b/tests/runtime/test_http_server.py
index 797dd3de..ea7616e2 100644
--- a/tests/runtime/test_http_server.py
+++ b/tests/runtime/test_http_server.py
@@ -366,7 +366,34 @@ def test_pydantic_wrapping_with_enums():
     assert token.text == "foo"
 
 
-def test_pydantic_wrapping_with_lists(runtime_http_server):
+def test_pydantic_wrapping_with_inheritance():
+    """Check that the pydantic wrapping works on our data models when they involve inheritance"""
+
+    @dataobject
+    class Base(DataObjectBase):
+        foo: int
+        bar: int
+
+    @dataobject
+    class Derived(Base):
+        bar: str
+        baz: str
+
+    # Check that our data model is fine and dandy
+    derived = Derived(bar="one", foo=2)
+    assert derived.bar == "one"
+    assert derived.foo == 2
+
+    # Wrap the containing data model in pydantic
+    pydantic_datamodel = http_server.RuntimeHTTPServer._dataobject_to_pydantic(Derived)
+
+    # Check that our data model is _still_ fine and dandy
+    new_derived = pydantic_datamodel(bar="one", foo=2)
+    assert new_derived.bar == "one"
+    assert new_derived.foo == 2
+
+
+def test_pydantic_wrapping_with_lists():
     """Check that pydantic wrapping works on data models with lists"""
 
     @dataobject(package="http")
@@ -380,7 +407,7 @@ def test_pydantic_wrapping_with_lists(runtime_http_server):
     foo = FooTest(bars=[BarTest(1)])
     assert foo.bars[0].baz == 1
 
-    runtime_http_server._dataobject_to_pydantic(FooTest)
+    http_server.RuntimeHTTPServer._dataobject_to_pydantic(FooTest)
 
     foo = FooTest(bars=[BarTest(1)])
     assert foo.bars[0].baz == 1