attrs icon indicating copy to clipboard operation
attrs copied to clipboard

Eliminate evolve() boilerplate with evolvers/copy

Open energizah opened this issue 6 years ago • 2 comments

When I want to evolve() an object, I sometimes have to do a bit of boilerplate copy()ing:

from typing import List

import attr


@attr.s
class Car:
    model: str = attr.ib()
    occupants: List = attr.ib()


car1 = Car("Toyota", ["Alice", "Bob"])
car2 = attr.evolve(car1, model="Honda", occupants=car1.occupants.copy())

To eliminate this boilerplate, one option would be:

@attr.s
class Car:
    model: str = attr.ib()
    occupants: List = attr.ib(evolver=lambda x: x.copy())

Or similarly:


@attr.s
class Car:
    model: str = attr.ib()
    occupants: List = attr.ib()

    @occupants
    def evolver(self):
        return self.occupants.copy()

Or using copy.copy() via the __copy__ method:

@attr.s
class Car:
    model: str = attr.ib()
    occupants: List = attr.ib(evolve_copy=True)
    

The latter idea suggests the option of putting the copy functionality into @attr.s, which could add a __copy__ method to the class, and like the other methods it could be enabled/disabled for each attribute with attr.ib(copy=True/False), and evolve() could use copy.copy() by default.

@attr.s(copy=True)
class Car:
    model: str = attr.ib()
    occupants: List = attr.ib()
    components: pyrsistent.PSet = attr.ib(copy=False)

cc: @altendky

energizah avatar Feb 11 '19 19:02 energizah

Hm, you basically want a deep copy for evolve? That sounds too complex to be in scope. Is there anything stopping you to implement it yourself on top of evolve?

hynek avatar Feb 24 '19 15:02 hynek

I can do most of it from outside, using the metadata field. The semantics are clearer for evolver= than for copy=. For example:

import copy
from typing import List

import attr


def fancy_evolve(obj, **kwargs):

    evolve_fields = {}
    for field_name, class_attrib in attr.fields_dict(type(obj)).items():
        if "evolver" not in class_attrib.metadata:
            continue

        evolver = class_attrib.metadata["evolver"]
        evolve_fields[field_name] = evolver(getattr(obj, field_name))

    evolve_fields.update(kwargs)
    return attr.evolve(obj, **evolve_fields)


# User code below.
##################

@attr.dataclass
class Driver:
    name: str
    age: int


@attr.s
class Vehicle:
    model: str = attr.ib()
    driver: Driver = attr.ib(metadata={"evolver": attr.evolve})
    parcel_ids: List = attr.ib(metadata={"evolver": copy.copy})


vehicle = Vehicle(model="Prius", driver=Driver("Alice", age=21), parcel_ids=[1, 4, 8])
new_vehicle = fancy_evolve(vehicle)

assert new_vehicle == vehicle
assert new_vehicle.driver == vehicle.driver
assert new_vehicle.parcel_ids == vehicle.parcel_ids
assert new_vehicle.driver is not vehicle.driver
assert new_vehicle.parcel_ids is not vehicle.parcel_ids

energizah avatar Feb 25 '19 04:02 energizah