BlackSheep
BlackSheep copied to clipboard
Graceful handling of extra properties from clients
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})
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)
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)