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.
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