BlackSheep icon indicating copy to clipboard operation
BlackSheep copied to clipboard

Graceful handling of extra properties from clients

Open RobertoPrevato opened this issue 1 year ago • 2 comments

Ignore extra properties when parsing binding input from the request payload?

Example for dataclasses:

from dataclasses import fields, is_dataclass


def create_instance(cls, props):
    """
    Creates an instance of a given dataclass type, ignoring extra properties.
    """
    if not is_dataclass(cls):
        raise ValueError("The given type is not a dataclass")
    class_fields = {f.name for f in fields(cls)}
    return cls(**{k: v for k, v in props.items() if k in class_fields})

RobertoPrevato avatar Jul 23 '22 14:07 RobertoPrevato

Note:

Python 3.10.4 (main, Apr  2 2022, 11:07:58) [GCC 9.3.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: class A:
   ...:     def __init__(self, a, b, c, **kwargs):
   ...:         self.a = a
   ...:         self.b = b
   ...:         self.c = c
   ...:

In [2]: from dataclasses import dataclass, fields

In [3]: def create_instance(cls, props):
   ...:     """
   ...:     Creates an instance of a given dataclass type, ignoring extra properties.
   ...:     """
   ...:     class_fields = {f.name for f in fields(cls)}
   ...:     return cls(**{k: v for k, v in props.items() if k in class_fields})
   ...:

In [4]: @dataclass
   ...: class B:
   ...:     a: int
   ...:     b: int
   ...:     c: int
   ...:

In [5]: A(**{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5})
Out[5]: <__main__.A at 0x7fd850abe200>

In [6]: data = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}

In [7]: a = A(**data)

In [8]: a.a
Out[8]: 1

In [9]: a.b
Out[9]: 2

In [10]: a.c
Out[10]: 3

In [11]: %timeit A(**data)
420 ns ± 1.45 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

In [12]: b = create_instance(B, data)

In [13]: b.a
Out[13]: 1

In [14]: b.b
Out[14]: 2

In [15]: b.c
Out[15]: 3

In [16]: %timeit A(**data)
419 ns ± 0.351 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

In [17]: B(**data)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [17], in <cell line: 1>()
----> 1 B(**data)

TypeError: B.__init__() got an unexpected keyword argument 'd'

In [18]: %timeit create_instance(B, data)
1.38 µs ± 3.22 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

In [19]: class_fields = {f.name for f in fields(B)}

In [20]: class_fields
Out[20]: {'a', 'b', 'c'}

In [21]: B(**{k: v for k, v in data.items() if k in class_fields})
Out[21]: B(a=1, b=2, c=3)

In [22]: %timeit B(**{k: v for k, v in data.items() if k in class_fields})
657 ns ± 1.59 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

In [23]: %timeit B(**{k: v for k, v in data.items() if k in class_fields})
651 ns ± 0.331 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

RobertoPrevato avatar Jul 27 '22 19:07 RobertoPrevato

In [24]: FIELDS = {}

In [27]: def get_fields(cls):
    ...:     try:
    ...:         return FIELDS[cls]
    ...:     except KeyError:
    ...:         FIELDS[cls] = {f.name for f in fields(cls)}
    ...:         return FIELDS[cls]
    ...:

In [28]:

In [28]: def create_instance(cls, props):
    ...:     """
    ...:     Creates an instance of a given dataclass type, ignoring extra properties.
    ...:     """
    ...:     class_fields = get_fields(cls)
    ...:     return cls(**{k: v for k, v in props.items() if k in class_fields})
    ...:

In [29]: %timeit create_instance(B, data)
796 ns ± 2.11 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

In [30]: FIELDS
Out[30]: {__main__.B: {'a', 'b', 'c'}}

In [31]: %timeit create_instance(B, data)
802 ns ± 6.04 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

RobertoPrevato avatar Jul 27 '22 19:07 RobertoPrevato