Search code examples
pythonpython-typingmypy

Type hinting of dependency injection


I'm creating a declarative http client and have a problem with mypy linting.

Error:

Incompatible default for argument "user" (default has type "Json", argument has type "dict\[Any, Any\]")

I have a "Dependency" class that implements the logic of: value validation agains type, request modification:

class Dependency(abc.ABC):
    def __init__(
        self,
        default: Any = Empty,
        field_name: Union[str, None] = None,
    ):
        self.default = default
        self._overridden_field_name = field_name

    ...

    @abc.abstractmethod
    def modify_request(self, request: RawRequest) -> RawRequest:
        raise NotImplementedError

The dependencies inherited from Dependency, e.g. Json:

class Json(Dependency):
    location = Location.json

    def __init__(self, default: Any = Empty):
        """Field name is unused for Json."""
        super().__init__(default=default)

    def modify_request(self, request: "RawRequest") -> "RawRequest":
        ...
        return request

Then I use them as function argument's default to declare:

@http("GET", "/example")
def test_get(data: dict = Json()):
    ...

It works as expected, but mypy is raising a lot of errors.

The question - how to deal with type hinting?

I need it to work like Query() or Body() in FastAPI, without changing the way of declaration.

I tried to make a Dependency class to be generic, but it wasn't helped me.

UPD:

Sorry, forgot to mention that type hint can be dataclass, or pydantic model, or any other type. Then it will be deserialized in function execution.

Dict as type annotation:

@http("GET", "/example")
def test_get(data: dict = Json()):
    ...

Pydantic model as type annotation:

class PydanticModel(BaseModel):
   …

@http("GET", "/example")
def test_get(data: PydanticModel = Json()):
    ...

Dataclass as type annotation:

@dataclasses.dataclass
class DataclassModel():
   …


@http("GET", "/example")
def test_get(data: DataclassModel = Json()):
    ...

It should support any type provided in type hint.


Solution

  • Solved using typing.Annotated, declaration has been changed a bit, but it works correctly and mypy has no errors to show.

    @http("GET", "/example")
    def test_get(data: Annotated[DataclassModel, Json()]):
        ...
    

    Then I'm using a function signature to extract dependency, type hint and do magic...

    def extract_dependencies(func: Callable):
        signature = inspect.signature(func)
        for key, val in signature.parameters.items():
            if key in ["self", "cls"]:
                # We don't need the self or cls parameter.
                continue
    
            # We check if the parameter is annotated.
            annotation = func.__annotations__.get(key, None)
            if hasattr(annotation, "__metadata__"):
                # Extracting the type hint and the dependency from the
                # Annotated type.
                type_hint, dependency = get_args(annotation)
                if not isinstance(dependency, Dependency):
                    if inspect.isclass(dependency) and issubclass(
                        dependency, Dependency
                    ):
                        # If the dependency is a class, we instantiate it.
                        dependency = dependency()
                        dependency.type_hint = type_hint
                    else:
                        # If the dependency is not an instance of Dependency,
                        # we raise an AnnotationException.
                        raise AnnotationException(annotation)
                else:
                    # If the dependency is already an instance of Dependency,
                    # we are setting only the type hint.
                    dependency.type_hint = type_hint
            else:
                ...
            dependency.field_name = key
            dependency.value = values.get(key, val.default)
            yield dependency