cattrs icon indicating copy to clipboard operation
cattrs copied to clipboard

Possible to support attr.ib(init=False) ?

Open mooncake4132 opened this issue 5 years ago • 15 comments

  • cattrs version: 0.9.0
  • Python version: 3.7.0
  • Operating System: Windows

Description

Right now cattr can only convert dictionaries back to the respective attr object if the field can be initialized in __init__ (because it calls cl(**conv_obj)). Is it possible for cattr to work with fields with init set to False?

What I Did

Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import attr
>>> import cattr
>>> @attr.s
... class A:
...     a: int = attr.ib(init=False)
...
>>> a = A()
>>> a.a = 10
>>> s = cattr.unstructure(a)
>>> cattr.structure(s, A)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\.virtualenvs\test\lib\site-packages\cattr\converters.py", line 178, in structure
    return self._structure_func.dispatch(cl)(obj, cl)
  File "D:\.virtualenvs\test\lib\site-packages\cattr\converters.py", line 298, in structure_attrs_fromdict
    return cl(**conv_obj)
TypeError: __init__() got an unexpected keyword argument 'a'
>>>

mooncake4132 avatar Aug 21 '18 05:08 mooncake4132

Also interested in a fix for this.

@Tinche Would by-passing init=False fields on both structure and unstructure make sense to you?

asford avatar Mar 17 '21 04:03 asford

@asford The fix needs to be here: https://github.com/Tinche/cattrs/blob/161dbd7b0ebd36123480ea770efab1929b2e57c6/src/cattr/gen.py#L151 (I guess just skipping attributes which are init=False).

Tinche avatar Mar 17 '21 11:03 Tinche

I might start on this soonish. But what should the spec be? If the key is present in the payload, use a setter instead of the initializer? Or should we just ignore fields with init=False completely when structuring?

Tinche avatar Mar 22 '21 11:03 Tinche

I vote for just skip, I heavily use and abuse frozen=True so a setter would explode if I managed to get into that situation. Could this somehow move into an option for the new GenConverter stuff if you wanted flexibility?

wakemaster39 avatar Mar 23 '21 13:03 wakemaster39

Yeah I agree, let's skip for now and if there's demand we can revisit later with an option.

Tinche avatar Mar 23 '21 14:03 Tinche

Fully agree, this is the ideal behavior on our end for structure.

Would the plan be to skip init=False on both structure and unstructure?

In the unstructure path cattr (I believe) currently includes all fields. This seems subtly wrong is the intention is to then skip the fields on structure.

On Tue, Mar 23, 2021, 07:40 Tin Tvrtković @.***> wrote:

Yeah I agree, let's skip for now and if there's demand we can revisit later with an option.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Tinche/cattrs/issues/40#issuecomment-804958011, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACFBKE5ACIXPP44QH2FV2LTFCR57ANCNFSM4FQUPLZA .

asford avatar Mar 23 '21 17:03 asford

Just out of curiosity, what do you folks use init=False fields for? Computed attributes set in post init?

Tinche avatar Mar 23 '21 22:03 Tinche

constants, if I type something with auto_attribs=True set it automatically becomes able to be set during initialization. So I have to use init=False to ensure it is not mutable until frozen=True takes over and protects it.

I use it very rarely for this reason, but its the only one I have had so far.

wakemaster39 avatar Mar 23 '21 22:03 wakemaster39

Hm, can you give an example? Your use case sounds like a ClassVar would be appropriate?

Tinche avatar Mar 23 '21 22:03 Tinche

oh 1000% a class var does the same thing. I don't use it often so I usually remember to just do and forget I should do something else.

@attrs(auto_attribs=True, frozen=True)
class Test:
    a: str
    b: str = attrib(init=False, default="QQ")

wakemaster39 avatar Mar 23 '21 22:03 wakemaster39

Just out of curiosity, what do you folks use init=False fields for? Computed attributes set in post init?

Yes. I have a transpiler, most of component's attr are computed and are optional

swuecho avatar Mar 25 '21 06:03 swuecho

I just found this issue because I'm running into this exact problem.

I use init=False attributes to create a "type" field to help with serialization. I don't want users to be able to change it, but cattrs seems to complain when it is included. Had to resort to creating a special hook to remove the "type" field after determining the appropriate class.

jpsnyder avatar Mar 26 '21 15:03 jpsnyder

@jpsnyder Interesting. Could you remove the field and create an unstructuring hook that adds it, for serialization?

Tinche avatar Mar 26 '21 16:03 Tinche

It looks like I don't need to have the type field anymore using your example in #140 Thanks again!

However, I would still vote for supporting init=False. I also had a use for it when I wanted to allow users to add "tags" to their constructed object, but didn't want to expose the tags in the init. However, this is purely a design decision in an effort to separate the two ideas with syntactic sugar.

@attr.define
class Element:
    tags: Set[str] = attr.ib(init=False, factory=set)

    def add_tag(self, *tags: Iterable[str]):
        for tag in tags:
            self.tags.add(tag)
        return self
        
 @attr.define
 class SubOne(Element):
     a: int
        
obj = SubOne(1).add_tag("apple", "banana")

jpsnyder avatar Mar 26 '21 18:03 jpsnyder

Any update on this?

@attr.define()
class Dataset:
    """A Dataset is used for marking data dependencies between workflows."""

    uri: str
    extra: Optional[Dict[str, Any]] = None


@define()
class File(Dataset):
    path: str
    conn_id: Optional[str] = None
    filetype: Optional[constants.FileType] = None
    normalize_config: Optional[Dict] = None

    uri: str = field(init=False, repr=False)
    extra: Optional[Dict] = field(init=False, factory=dict, repr=False)

This fails with :

  |   File "/usr/local/lib/python3.9/site-packages/airflow/lineage/__init__.py", line 75, in _render_object
  |     return structure(
  |   File "/usr/local/lib/python3.9/site-packages/cattrs/converters.py", line 281, in structure
  |     return self._structure_func.dispatch(cl)(obj, cl)
  |   File "<cattrs generated structure astro.files.base.File>", line 44, in structure_File
  |     except Exception as exc: raise __c_cve('While structuring File', [exc], __cl)
  | cattrs.errors.ClassValidationError: While structuring File (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<cattrs generated structure astro.files.base.File>", line 41, in structure_File
    |     return __cl(
    | TypeError: __init__() got an unexpected keyword argument 'uri'
    +------------------------------------

kaxil avatar Sep 02 '22 11:09 kaxil

Hi, I would be also interested in this feature. To answer your question, @Tinche:

Just out of curiosity, what do you folks use init=False fields for? Computed attributes set in post init?

Yes, this is my primary use case. I think there are many situations where post init is required for a clean code structure and it would be awesome if cattrs would handle these situations automatically (because I think the only other thing someone would do in this situation is to take the boilerplate route and add the trivial structure hooks manually).

Or is there any better way to achieve the same with the current capabilities?

AdrianSosic avatar Apr 14 '23 13:04 AdrianSosic

Hello ! Any update on this?

coreegor avatar Jul 05 '23 12:07 coreegor

It'll be in the next milestone, alongside new attrs aliases!

Tinche avatar Jul 05 '23 13:07 Tinche

Aaaand merged!

Tinche avatar Jul 17 '23 18:07 Tinche