attrs
attrs copied to clipboard
Eliminate evolve() boilerplate with evolvers/copy
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
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?
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