Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Addreuse() toAttribute for field evolution#1429

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
redruin1 wants to merge7 commits intopython-attrs:main
base:main
Choose a base branch
Loading
fromredruin1:evolve-inherited-fields

Conversation

@redruin1
Copy link
Contributor

@redruin1redruin1 commentedMay 2, 2025
edited
Loading

Summary

Preliminary implementation of "evolving" existingAttribute instances back into_CountingAttr instances, so that users may (easily) reuse attribute definitions from alreadydefined classes. Shouldresolve#637,#698,#829,#876,#946, and#1424.

Usage:

@attrs.defineclassParent:a:int=100@attrs.defineclassChild:a=attrs.fields(Parent).a.evolve(default=200).to_field()# Now that a is in scope of Child, we can attach methods to it like normal:@a.validatordefa_validator(...):        ...assertattrs.fields(Child).a.typeisintassertChild().a==200

This syntax is verbose, but least magical. And because you manually specify which class you want to pull attributes from, inheritance is not required; you can compose new classes entirely from parts of existing classes regardless of structure:

@attrs.defineclassA:a:int=1@attrs.defineclassB:b:int=2@attrs.defineclassComposite:a=attrs.fields(A).a.evolve(...).to_field()b=attrs.fields(B).b.evolve(...).to_field()

Utility methods likeattrs.make_class() and thethese kwarg inattrs.define() also work as you would expect.

One potential pain point with a simple implementation of this feature is inherited attributes being reordered when redefined in this manner:

@attr.sclassBaseClass:x:int=attr.ib(default=1)y:int=attr.ib(default=2)@attr.sclassSubClass(BaseClass):x=attr.fields(BaseClass).x.evolve(default=3).to_field()# Because x was redefined, it appears after yassert"SubClass(y=2, x=3)"==repr(SubClass())

In my opinion, this behavior is a failure of the implementation, as it is reasonable to expect users to want to preserve this ordering, as in a worst-case scenario it can lead to non-constructable classes. This PR implements a new bool keyword argumentinherited tofield, which tells attrs to use the ordering of the field in the parent class as opposed to adding to the end:

@attr.sclassSubClass(BaseClass):x=attr.fields(BaseClass).x.evolve(default=3,inherited=True).to_field()assert"SubClass(x=3, y=2)"==repr(SubClass())

Criticisms of thisinherited keyword are welcome, as I'm not convinced it is the best solution. However, I do think this functionality needs to be present in attrs in order to make this kind of attribute evolution a tenable approach.

Pull Request Check List

  • Donot open pull requests from yourmain branch –use a separate branch!
  • Addedtests for changed code.
  • New features have been added to ourHypothesis testing strategy.
  • Changes or additions to public APIs are reflected in our type stubs (files ending in.pyi).
    • ...and used in the stub test filetests/typing_example.py.
    • If they've been added toattr/__init__.pyi, they'vealso been re-imported inattrs/__init__.pyi.
  • Updateddocumentation for changed code.
    • New functions/classes have to be added todocs/api.rst by hand.
    • Changes to the signatures of@attr.s() and@attrs.define() have to be added by hand too.
    • Changed/added classes/methods/functions have appropriateversionadded,versionchanged, ordeprecateddirectives.
      The next version is the second number in the current release + 1.
      The first number represents the current year.
      So if the current version on PyPI is 22.2.0, the next version is gonna be 22.3.0.
      If the next version is the first in the new year, it'll be 23.1.0.
      • If something changed that affects bothattrs.define() andattr.s(), you have to add version directives to both.
  • Documentation in.rst and.md files is written usingsemantic newlines.
  • Changes (and possible deprecations) have news fragments inchangelog.d.
  • Consider grantingpush permissions to the PR branch, so maintainers can fix minor issues themselves without pestering you.

redruin1and others added2 commitsMay 2, 2025 13:02
with additional `inherited` kwarg to preserve inherited class order
@Tinche
Copy link
Member

Why does it need to beattrs.fields(Parent).a.evolve(default=200).to_field() instead of maybeattrs.fields(Parent).a.to_field(default=200)? We'll need to add Mypy support to this before it can be used for real, and we should look at making that as easy as possible.

redruin1 reacted with thumbs up emoji

@redruin1
Copy link
ContributorAuthor

If the goal is to reduce function chains, then how about the keywordreuse()? That might be more descriptive in this context thanto_field():

classA:x=attrs.fields(Example).x.reuse()y=attrs.fields(Example).y.reuse(default="whatever")_z=attrs.fields(OtherExample).a.reuse(alias="z")

@Tinche
Copy link
Member

I like it!

redruin1 reacted with heart emoji

@redruin1
Copy link
ContributorAuthor

What exactly should I annotate for mypy? I notice that neither the return result ofattr.fields() nor_CountingAttr itself have annotations already:

deffields(cls:type[AttrsInstance])->Any: ...

Should I just try to annotate everything this new feature touches? The contribution guidelines are a bit sparse in this regard.

redruin1and others added4 commitsMay 7, 2025 20:03
* Added material in "Other goodies" section in `examples.md`* docstrings for `field`, `attrib`, and `reuse`*Added towncrier changelog message
@redruin1redruin1 marked this pull request as ready for reviewJune 10, 2025 20:00
@redruin1redruin1 changed the titleAddto_field() toAttributeAddreuse() toAttribute for field evolutionJun 10, 2025
@hynek
Copy link
Member

Soy = attrs.fields(Example).y.reuse(default="whatever") is cool indeed – I'm just not sure if that will ever be possible to get into typing at all??

I'm kinda OK with saying that you have to redefine your types, otherwise it gets hairy with ordering anyways (or some magic helper type, e.g.,y: Annotated[Inherit] = attrs.fields(Example).y.reuse(default="whatever")).

But how could any of this be made type-safe?y: Annotated[Inherit(attrs.fields(Example).y, default="whatever")] would be probably even cooler since it circumvents the ordering problems, but I guess we should think ahead here? Does maybe Pydantic have something similar so we could join PEP forces or something?

@Tinche
Copy link
Member

We could contribute work to the mypy plugin so it would work 😇

@redruin1
Copy link
ContributorAuthor

I'm kinda OK with saying that you have to redefine your types, otherwise it gets hairy with ordering anyways (or some magic helper type, e.g., y: Annotated[Inherit] = attrs.fields(Example).y.reuse(default="whatever")).

This would make sense. The only reason it is omitted in this particular implementation is because the type is already inherited from thereuse() call and (re)including a type annotation would be akin to defining it twice, which attrs complains about. This can simply be rewritten to not inherit the type, and to default toAny if no type annotation is provided.

y: Annotated[Inherit(attrs.fields(Example).y, default="whatever")] would be probably even cooler since it circumvents the ordering problems

This is not a bad idea. If I understand you correctly, does that make the proposed syntax:

@attrs.defineclassA:x:int=10y:str=attrs.field(default="blah")@attrs.defineclassB:z:dict= {"test":"result"}@attrs.defineclassChild(B,A):x:Annotated[Inherited(attrs.fields(A).x,default=20)]# Preserve parent orderingy:str=attrs.fields(A).y.reuse()# Add to end after inheritedz:Annotated[Inherited(attrs.fields(B).z,factory=dict)]# Preserve parent orderingassert"Child(x=20, z={}, y='blah')"==repr(Child())

It seems the question of what order to put inherited fields in is not trivial. I suppose you could argue that if you need absolute control of field order, then you should create an entirely composite class usingreuse() and ignore inheritance entirely, which violates DRY a little but keeps things obvious.

We could contribute work to the mypy plugin so it would work 😇

I'll look into this. I assume you're referring tothis?

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Option to "partially override" an attribute from a parent class?

3 participants

@redruin1@Tinche@hynek

[8]ページ先頭

©2009-2025 Movatter.jp