python-attrs / cattrs

Composable custom class converters for attrs, dataclasses and friends.
https://catt.rs
MIT License
808 stars 113 forks source link

Detailed validation exception groups with hook errors #514

Closed salotz closed 7 months ago

salotz commented 7 months ago

Description

I would like errors in custom hooks to be added to the detailed validation exceptions.

What I Did

import attrs
import cattrs

@attrs.define
class Thing:
    a: int

def bad_hook(*args, **kwargs):

    raise ValueError("duhhh")

converter = cattrs.Converter(detailed_validation=True)

converter.register_unstructure_hook(int, bad_hook)

converter.unstructure(Thing(a=1))

Raises:

Traceback (most recent call last):
  File "/home/salotz/scratch/cattrs-error.py", line 16, in <module>
    converter.unstructure(Thing(a=1))
  File "/home/salotz/.../.venv/lib/python3.11/site-packages/cattrs/converters.py", line 238, in unstructure
    return self._unstructure_func.dispatch(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<cattrs generated unstructure __main__.Thing>", line 3, in unstructure_Thing
  File "/home/salotz/scratch/cattrs-error.py", line 10, in bad_hook
    raise ValueError("duhhh")
ValueError: duhhh

And as a control:

converter.structure({"b" : 1}, Thing)

Raises the expected:

  + Exception Group Traceback (most recent call last):
  |   File "/home/salotz/tree/ibeks/devel/bos/.venv/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3577, in run_code
  |     exec(code_obj, self.user_global_ns, self.user_ns)
  |   File "<ipython-input-3-559339a52653>", line 1, in <module>
  |     converter.structure({"b" : 1}, Thing)
  |   File "/home/salotz/tree/ibeks/devel/bos/.venv/lib/python3.11/site-packages/cattrs/converters.py", line 332, in structure
  |     return self._structure_func.dispatch(cl)(obj, cl)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "<cattrs generated structure __main__.Thing>", line 9, in structure_Thing
  |     if errors: raise __c_cve('While structuring ' + 'Thing', errors, __cl)
  |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  | cattrs.errors.ClassValidationError: While structuring Thing (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<cattrs generated structure __main__.Thing>", line 5, in structure_Thing
    |     res['a'] = __c_structure_a(o['a'])
    |                                 ~^^^^^
    | KeyError: 'a'
    | Structuring class Thing @ attribute a
    +------------------------------------

Maybe there is a way to do this already I missed?

Tinche commented 7 months ago

Hi!

Detailed validation only applies to structuring hooks.

This is a deliberate design choice, for simplicity and speed. Structuring usually applies to untrusted data entering your system so we support more sophisticated ways of dealing with it. Unstructuring usually applies to data exiting the system, which has usually already been checked either by structuring or by static analysis like Mypy. This is why, for example, cattrs will call str on string fields when structuring but will (by default) just pass the value through when unstructuring.

Sorry :/

salotz commented 7 months ago

That makes sense :) That is one of the reasons I chose cattrs, I guess I just hadn't thought through it for this case.