-
-
Notifications
You must be signed in to change notification settings - Fork 374
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
API semantics for deep nested evolve calls #932
Comments
Definitely an interesting idea, but |
Thanks @Tinche . So maybe a string path? foo = attr.focus(foo,'bar.baz.qux').modify('yay!') |
This might be an alternative approach to #634 and #861. It shouldn't need any special support within attrs? Modify seems to be a callable thing (replace is for setting), but that should be easy enough with Python. cc @sscherfke |
I just made a separate function based on #634 (comment), and it seems to work fine: 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:]
value = getattr(inst, attr_name)
if init_name not in changes:
# Add original value to changes
changes[init_name] = value
elif attr.has(value):
# Evolve nested attrs classes
changes[init_name] = attr.evolve(value, **changes[init_name])
return cls(**changes)
def test_set_deep_sparse():
@attr.s(auto_attribs=True, frozen=True, slots=True)
class Bar:
a: str
b: str
@attr.s(auto_attribs=True, frozen=True, slots=True)
class Baz:
c: str
d: str
@attr.s(auto_attribs=True, frozen=True, slots=True)
class Foo:
bar: Bar
baz: Baz
foo = Foo(bar=Bar(a='a1', b='b1'), baz=Baz(c='c1', d='d1'))
assert (evolve_recursive(foo, bar={'a': 'a2'}, baz={'c': 'c2'}) ==
Foo(bar=Bar(a='a2', b='b1'), baz=Baz(c='c2', d='d1'))) |
Typed Settings contains a recursive version of Typed Settings also defines operations for working with dicts and dotted paths which is a bit similar to @jacobg 's original idea. Updating nested instances with a dotted path may be easier if you just want to update single attributes. The recursive evolve is a bit more convenient when you want to update multiple attributes (on different nesting levels). So maybe there's room for both variants. |
If it's syntactically nicer, one can use the evolve_recursive(foo, bar=dict(a="a2"), baz=dict(c="c2")) In fact one could define a class Evolve that just wrapped its arguments, and then it could be used backward-compatibly in evolve(foo, bar=Evolve(a="a2"), baz=Evolve(c="c2")) That is, Unfortunately, this won't allow static type checking of the arguments to Evolve. But then I don't really think it's feasible to statically type This would look something like: class Evolve:
def __init__(self, **changes):
self.changes = changes def evolve(inst, **changes):
cls = inst.__class__
attrs = fields(cls)
for a in attrs:
if not a.init:
continue
attr_name = a.name # To deal with private attributes.
init_name = a.alias
if init_name not in changes:
changes[init_name] = getattr(inst, attr_name)
elif isinstance(changes[init_name], Evolve):
changes[init_name] = evolve(getattr(inst, attr_name), changes[init_name].changes)
return cls(**changes) If one wanted to be more ambitious, one could make evolve(foo, bar=evolve(a="a2"), baz=evolve(c="c2")) Without additional work this would be a problem for error handling, because if one forgot the positional argument the effect would be very peculiar. It might make more sense to give new_thing = Evolve(bar=Evolve(a="a2"), baz=Evolve(c="c2")).modify(old_thing) This would coexist fairly well with static type checking, in that an Evolve object would be an obviously different type from the instance we were modifying. In principle it might be possible to make Evolve generic in a way that type inference could work for it. |
I don't think given the increasingly narrow guardrails that are forced on us by Pyright/dataclass transform, thinking about type-safety is a waste of time. :( The builder approach sure looks interesting, so if someone wants to tackle it, why not add a third way to copy-and-modify instances. ;) |
Incidentally, I implemented exactly this approach some months ago. Here is the code: https://gist.github.com/loehnertj/4cf864d98054d7a54749e99bd4ae28b8 Documentation and some testing included. Use it as you like. I'll gladly help with integration if desired (however I don't know about attrs' internal structure + standards). One particular challenge was how to handle list-of-object fields, which occur rather often. This is solved by means of a second "evols" (Evolve-list) function giving you a "builder"-ish interface to modify lists. I.e. you chain method calls to Quoting the docstrings:
This is very similar to The first argument may be omitted. In this case, returns a callable that Secondly, if any keyword argument-value is an Evolution instance, it will be This means, that you can save a lot of boilerplate repetition when evolving Example: Suppose that object Classical
With
Example: suppose that the
To actually specify the operations to do, chain-call the appropriate methods
Other possible operations are adding, removing, reordering items as well as Optionally, return type of the modification can be specified by means of List evolution methods include |
When trying to evolve a frozen object at a deep level, there is a lot of boilerplate, e.g.,:
Scala has a similar problem, being that immutable data classes is a normal thing in that language. There is a Scala library developed helped to solve that problem: https://www.optics.dev/Monocle/
Maybe it would be useful to have those sort of semantics for updating frozen attr objects,e.g.:
Any thoughts?
The text was updated successfully, but these errors were encountered: