In my initial post, I wrote:
However, I am wondering if there might not be a better way to do this argument validation. Potentially using a third party library. I guess my point is that I am feeling that I might be reinventing the wheel and that people smarte than me likely faced this issue before and probably implemented a better solution.
I found that I could use either attrs, pydantic, or beartype to do what I wanted so I implemented solutions using each to test them and decide for myself what seems to make more sense for my project. typeguard was another alternative which I haven't tested.
Below are the three implementations.
import attrs
@attrs.define
class GetAttrs:
id: str | list[str] = attrs.field(
validator=attrs.validators.or_(
attrs.validators.instance_of(str),
attrs.validators.deep_iterable(
member_validator=attrs.validators.instance_of(str),
iterable_validator=attrs.validators.instance_of(list),
),
)
)
id_type: str = attrs.field(validator=attrs.validators.instance_of(str))
ctry_code: str = attrs.field(default = "", validator=attrs.validators.matches_re(r"[A-Z]{3}$|^LOCAL$|^$"))
is_alive: bool = attrs.field(default = False, validator=attrs.validators.instance_of(bool))
def get(self, **kwargs):
# doing some stuff
object1 = Object(kwargs)
# doing some other stuff to get data
return data
GetObj = GetAttrs(id=["id1", "id2"], id_type="myidtype")
GetObj.get(kv1={"k1": 1, "k2": 2}, kv2="test")
My main issue with this solution is that I have to pass kwargs to the method get()
and not at the instanciation of GetAttrs
. From what I found, I could pass kwarg at the instanciation of GetAttrs
but it does not seems super clean.
However, I good point is that as I am now planning to use attrs
in my modules, using it here would not lead to multiplication of third party libraries.
from typing import Literal
from pydantic import Field, validate_call
type Liststr = list[str]
@validate_call
def get_pydantic(id: str | Liststr,
id_type: str,
ctry_code: str = Field(default="", pattern=r"^[A-Z]{3}$|^LOCAL$|^$"),
is_alive: bool = False,
**kwargs ):
# doing some stuff
object1 = Object(kwargs)
# doing some other stuff to get data
return data
get_pydantic("id"=["id1", "id2"], id_type="myidtype", kv1={"k1": 1, "k2": 2}, kv2="test")
Works quite well and I have no real concern with this solution. However, the decorator @validate_call
is a very tiny part of pydantic, which does many other things, so it might make sense to use something dedicated to argument validation with a bit less scope.
from typing import Annotated, Literal, Union
import re
from beartype import beartype
from beartype.cave import IterableType
from beartype.vale import Is
IsCtry = Annotated[str, Is[lambda string: re.fullmatch(r"[A-Z]{3}$|^LOCAL$|^$", string)]]
@beartype
def get_beartype(id: Union[str, IterableType[str]],
id_type: str,
item_type: str,
ctry_code: IsCtry = "",
is_alive: bool = False,
**kwargs
):
# doing some stuff
object1 = Object(kwargs)
# doing some other stuff to get data
return data
get_beartype(id=["id1", "id2"], id_type="myidtype", kv1={"k1": 1, "k2": 2}, kv2="test")
Works quite well and I found it quite elegant. I do not see any strong issue and as I want to use attrs
in my module, I have the impression that I don't have the strong overlap with it that pydantic
might have. So I have decided to use it as my solution.
Hope that helps anyone who might have a similar problem. Further any feedback on how thos implementations could be improved is very welcome.