Skip to content

Recursive attr.evolve #634

Open
Open

Description

@sveinse

In the same manner as attr.asdict(..., recursive=True) exists, I have a use case for a similar option to attr.evolve(). I want all attr instances evolved, but with no need to deepcopy any non-attr attributes. I ended up implementing the code below. Could this be a consideration for addition into python-attrs?

def evolve_recursive(inst, **changes):
    """ Recursive attr.evolve() method, where any attr-based attributes
        will be evolved too.
    """
    cls = inst.__class__
    attrs = attr.fields(cls)
    for a in attrs:
        if not a.init:
            continue
        attr_name = a.name  # To deal with private attributes.
        init_name = attr_name if attr_name[0] != "_" else attr_name[1:]
        if init_name not in changes:
            value = getattr(inst, attr_name)

            # New code
            if attr.has(value.__class__):
                value = evolve_recursive(value)

            changes[init_name] = value
    return cls(**changes)

Activity

  1. hynek commented on Oct 19, 2020

    @hynek
    Member

    Sorry for the late answer. Would you mind showing/elaborating some use cases for this?

  2. sscherfke commented on Oct 19, 2020

    @sscherfke
    Contributor

    I’ve had a similar issue in typed settings when I want to updated nested settings and fixed it by serializing instances to dicts, calling a _set_path function and later converting the dicts back to attrs instances (which only works when using (auto) converters, though).

  3. hynek commented on Oct 30, 2020

    @hynek
    Member

    We have a volunteer! ✨

  4. sscherfke commented on Oct 30, 2020

    @sscherfke
    Contributor

    Wat? 👀

  5. sscherfke commented on Dec 13, 2020

    @sscherfke
    Contributor

    attr.evolve() can be adjusted relatively easy to work recursively:

    diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py
    index 56e3fcf..1cac98e 100644
    --- a/src/attr/_funcs.py
    +++ b/src/attr/_funcs.py
    @@ -333,8 +333,13 @@ def evolve(inst, **changes):
                 continue
             attr_name = a.name  # To deal with private attributes.
             init_name = attr_name if attr_name[0] != "_" else attr_name[1:]
    +        value = getattr(inst, attr_name)
             if init_name not in changes:
    -            changes[init_name] = getattr(inst, attr_name)
    +            # Add original value to changes
    +            changes[init_name] = value
    +        elif has(value):
    +            # Evolve nested attrs classes
    +            changes[init_name] = evolve(value, **changes[init_name])
    
         return cls(**changes)
    

    I’m not sure whether this is a backwards incompatible change or rather a bug fix that prevents nested classes from being replaced by a dict. 🤷‍♂️

  6. sscherfke commented on Dec 19, 2020

    @sscherfke
    Contributor

    @hynek What are your requirements regarding the public API?

    Do you prefer a new function or would you rather update the behavior of the existing one as in the diff above?

  7. hynek commented on Dec 21, 2020

    @hynek
    Member

    I think if you can do it backward compatible, there's no need for a new function?

  8. sscherfke commented on Dec 21, 2020

    @sscherfke
    Contributor

    I guess, it’ll be mostly backward compatible. *mostly, because handling of nested dicts will be different if they map to attributes with attrs classes. ;-)

    But the above changes did not break any of the existing tests.

    I’ll do a PR once I figure out how to proberly use hypothesis and nested classes for the tests.

  9. added 3 commits that reference this issue on Feb 14, 2021
    695a0bc
    acf0aed
    53c1d7a
  10. added a commit that references this issue on Feb 19, 2021
    fe6eb31
  11. reopened this on May 6, 2021
  12. hynek commented on May 6, 2021

    @hynek
    Member

    Unfortunately we had to revert the changes due to #804. I think we'll have to write a new function to make stuff unequivocal.

    Sorry everyone! :(

  13. added 4 commits that reference this issue on Nov 3, 2021
    e698072
    6f104a5
    178cc80
    4a9b27f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions