Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Excluding attributes on an attrs model from unstructuring breaks auto unstructuring #169

Closed
mitchej123 opened this issue Sep 22, 2021 · 9 comments

Comments

@mitchej123
Copy link

  • cattrs version: 1.8.0
  • Python version: 3.9.2
  • Operating System: OSX - Big Sur 11.5.2

Description

I have a field on an attrs model that I want excluded when unstructuring. When registering an unstructure hook using make_dict_unstructure_fn and field=override(Skip=True) (as read in the documentation) the other unstructure hooks aren't being triggered (UUID, nested models, etc). The same issue happens if the example using register_unstructure_hook_factory is used instead.

What I Did

Sample program to demonstrate the issue. Running json.dumps on the unstructured Breaks will break complaining that Object of type UUID is not JSON serializable (or if the ID is commented out then Object of type Nested), whereas it works properly on Works

from __future__ import annotations

import json
from typing import Optional
from uuid import UUID, uuid4

import attr
from cattr import GenConverter
from cattr.gen import make_dict_unstructure_fn, override


@attr.s(auto_attribs=True, slots=True, frozen=True)
class Nested:
    nested_string: Optional[str] = attr.ib(factory=lambda: str(uuid4()))


@attr.s(auto_attribs=True, slots=True, frozen=True)
class Works(object):
    id: UUID = attr.ib(factory=uuid4)
    nested: Nested = attr.ib(factory=Nested)


@attr.s(auto_attribs=True, slots=True, frozen=True)
class Breaks(object):
    id: UUID = attr.ib(factory=uuid4)
    exclude_me: str = attr.ib(factory=lambda: str(uuid4()))
    nested: Nested = attr.ib(factory=Nested)


def _gen_conv() -> GenConverter:
    _conv = GenConverter()
    _conv.register_unstructure_hook(frozenset, lambda v: list(v))
    _conv.register_unstructure_hook(UUID, lambda v: str(v))

    # This handles skipping `exclude_me` properly, but other nested attr classes don't get properly unstructured
    _conv.register_unstructure_hook(Breaks, make_dict_unstructure_fn(Breaks, _conv, exclude_me=override(omit=True)))

    return _conv


def unstructure_breaks(breaks):
    doesnt_break = conv.unstructure(breaks)
    doesnt_break.pop("exclude_me", None)
    return doesnt_break


if __name__ == '__main__':
    conv = _gen_conv()
    print(json.dumps(conv.unstructure(Works())))
    print(json.dumps(conv.unstructure(Breaks()))) # Breaks because it can't serialize UUID/Nested
❯ python cattr_poc.py
{"id": "dbab1954-5338-40f1-a9f8-9ba8ec5a280d", "nested": {"nested_string": "e0317c43-c06e-44c8-b6df-fd992649a57c"}}
Traceback (most recent call last):
  File "/Users/jason/dev/cattr_poc.py", line 50, in <module>
    print(json.dumps(unstructure_breaks(Breaks())))
  File "/Users/jason/.pyenv/versions/3.9.2/lib/python3.9/json/__init__.py", line 231, in dumps
    return _default_encoder.encode(obj)
  File "/Users/jason/.pyenv/versions/3.9.2/lib/python3.9/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/Users/jason/.pyenv/versions/3.9.2/lib/python3.9/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/Users/jason/.pyenv/versions/3.9.2/lib/python3.9/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type UUID is not JSON serializable

What I would expect

I'm looking for a good way to exclude one or more attributes from unstructuring an attrs model without needing to wrap them in a function like this:

def unstructure_breaks(breaks):
    doesnt_break = conv.unstructure(breaks)
    doesnt_break.pop("exclude_me", None)
    return doesnt_break
@Tinche
Copy link
Member

Tinche commented Sep 22, 2021

Hello,

I'm on my honeymoon atm so my access to a computer is limited. Does it work if you remove the __future__ import?

@mitchej123
Copy link
Author

I'm on my honeymoon atm so my access to a computer is limited.

No worries!

Does it work if you remove the __future__ import?

Yes, if I remove the __future__ import it works. You mentioning that made me think of another similar issue that I wasn't able to reproduce in a minimal fashion (but was able to work around), and it looks like the same underlying issue and workaround! The attr.field() is returning {str} UUID instead of <Class UUID>, so the dispatch isn't able to find the <Class UUID> converter.

If I leave the __future__ import in, and change the _gen_conv function as follows it works as expected

def _gen_conv() -> GenConverter:
    _conv = GenConverter()
    _conv.register_unstructure_hook(frozenset, lambda v: list(v))
    _conv.register_unstructure_hook(UUID, lambda v: str(v))

    attr.resolve_types(Breaks) # Force resolution of the types so it doesn't fall back to string types
    _conv.register_unstructure_hook(Breaks, make_dict_unstructure_fn(Breaks, _conv, exclude_me=override(omit=True)))

    return _conv

Evidence that it's only finding a str type

Setting a breakpoint here, and looking at attr.fields(obj.__class__)

    def unstructure(self, obj: Any, unstructure_as=None) -> Any:
        return self._unstructure_func.dispatch(
            obj.__class__ if unstructure_as is None else unstructure_as
        )(obj)

image

image

@Tinche
Copy link
Member

Tinche commented Sep 23, 2021

Yeah, I will add annotation resolution to the gen functions in the next release. In the meantime, you can work around the issue by calling it yourself, as mentioned :)

@Tinche
Copy link
Member

Tinche commented Oct 14, 2021

This should work now on master, the gen functions will call resolve_types for you.

@mitchej123
Copy link
Author

Awesome thanks, will give it a try.

@anthrotype
Copy link

@Tinche Any chances that we can have a new release soon that includes this patch? Thanks in advace

@Tinche
Copy link
Member

Tinche commented Nov 18, 2021

Yeah, there will be a release this week!

@praateekmahajan
Copy link

Just saw this issue, seems similar to something I reported on attrs python-attrs/attrs#901

@Tinche any thoughts on that?

@Tinche Tinche closed this as completed Jan 4, 2022
@Tinche
Copy link
Member

Tinche commented Jan 4, 2022

This should now work with the latest release, forgot to close.

mergify bot pushed a commit to aws/jsii that referenced this issue Jan 6, 2022
…1 in /packages/@jsii/python-runtime (#3315)

Updates the requirements on [cattrs](https://github.com/python-attrs/cattrs) to permit the latest version.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/python-attrs/cattrs/blob/main/HISTORY.rst">cattrs's changelog</a>.</em></p>
<blockquote>
<h2>1.10.0 (2022-01-04)</h2>
<ul>
<li>Add PEP 563 (string annotations) support for dataclasses.
(<code>[#195](python-attrs/cattrs#195) &lt;https://github.com/python-attrs/cattrs/issues/195&gt;</code>_)</li>
<li>Fix handling of dictionaries with string Enum keys for bson, orjson, and tomlkit.</li>
<li>Rename the <code>cattr.gen.make_dict_unstructure_fn.omit_if_default</code> parameter to <code>_cattrs_omit_if_default</code>, for consistency. The <code>omit_if_default</code> parameters to <code>GenConverter</code> and <code>override</code> are unchanged.</li>
<li>Following the changes in <code>attrs</code> 21.3.0, add a <code>cattrs</code> package mirroring the existing <code>cattr</code> package. Both package names may be used as desired, and the <code>cattr</code> package isn't going away.</li>
</ul>
<h2>1.9.0 (2021-12-06)</h2>
<ul>
<li>Python 3.10 support, including support for the new union syntax (<code>A | B</code> vs <code>Union[A, B]</code>).</li>
<li>The <code>GenConverter</code> can now properly structure generic classes with generic collection fields.
(<code>[#149](python-attrs/cattrs#149) &lt;https://github.com/python-attrs/cattrs/issues/149&gt;</code>_)</li>
<li><code>omit=True</code> now also affects generated structuring functions.
(<code>[#166](python-attrs/cattrs#166) &lt;https://github.com/python-attrs/cattrs/issues/166&gt;</code>_)</li>
<li><code>cattr.gen.{make_dict_structure_fn, make_dict_unstructure_fn}</code> now resolve type annotations automatically when PEP 563 is used.
(<code>[#169](python-attrs/cattrs#169) &lt;https://github.com/python-attrs/cattrs/issues/169&gt;</code>_)</li>
<li>Protocols are now unstructured as their runtime types.
(<code>[#177](python-attrs/cattrs#177) &lt;https://github.com/python-attrs/cattrs/pull/177&gt;</code>_)</li>
<li>Fix an issue generating structuring functions with renaming and <code>_cattrs_forbid_extra_keys=True</code>.
(<code>[#190](python-attrs/cattrs#190) &lt;https://github.com/python-attrs/cattrs/issues/190&gt;</code>_)</li>
</ul>
<h2>1.8.0 (2021-08-13)</h2>
<ul>
<li>Fix <code>GenConverter</code> mapping structuring for unannotated dicts on Python 3.8.
(<code>[#151](python-attrs/cattrs#151) &lt;https://github.com/python-attrs/cattrs/issues/151&gt;</code>_)</li>
<li>The source code for generated un/structuring functions is stored in the <code>linecache</code> cache, which enables more informative stack traces when un/structuring errors happen using the <code>GenConverter</code>. This behavior can optionally be disabled to save memory.</li>
<li>Support using the attr converter callback during structure.
By default, this is a method of last resort, but it can be elevated to the default by setting <code>prefer_attrib_converters=True</code> on <code>Converter</code> or <code>GenConverter</code>.
(<code>[#138](python-attrs/cattrs#138) &lt;https://github.com/python-attrs/cattrs/issues/138&gt;</code>_)</li>
<li>Fix structuring recursive classes.
(<code>[#159](python-attrs/cattrs#159) &lt;https://github.com/python-attrs/cattrs/issues/159&gt;</code>_)</li>
<li>Converters now support un/structuring hook factories. This is the most powerful and complex venue for customizing un/structuring. This had previously been an internal feature.</li>
<li>The <code>Common Usage Examples &lt;https://cattrs.readthedocs.io/en/latest/usage.html#using-factory-hooks&gt;</code>_ documentation page now has a section on advanced hook factory usage.</li>
<li><code>cattr.override</code> now supports the <code>omit</code> parameter, which makes <code>cattrs</code> skip the atribute entirely when unstructuring.</li>
<li>The <code>cattr.preconf.bson</code> module is now tested against the <code>bson</code> module bundled with the <code>pymongo</code> package, because that package is much more popular than the standalone PyPI <code>bson</code> package.</li>
</ul>
<h2>1.7.1 (2021-05-28)</h2>
<ul>
<li><code>Literal</code> s are not supported on Python 3.9.0 (supported on 3.9.1 and later), so we skip importing them there.
(<code>[#150](python-attrs/cattrs#150) &lt;https://github.com/python-attrs/cattrs/issues/150&gt;</code>_)</li>
</ul>
<h2>1.7.0 (2021-05-26)</h2>
<ul>
<li><code>cattr.global_converter</code> (which provides <code>cattr.unstructure</code>, <code>cattr.structure</code> etc.) is now an instance of <code>cattr.GenConverter</code>.</li>
<li><code>Literal</code> s are now supported and validated when structuring.</li>
<li>Fix dependency metadata information for <code>attrs</code>.
(<code>[