Skip to content

Add to_field() to Attribute #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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

redruin1
Copy link

@redruin1 redruin1 commented May 2, 2025

Summary

Preliminary implementation of "evolving" existing Attribute instances back into _CountingAttr instances, so that users may (easily) reuse attribute definitions from already defined classes. Should resolve #637, #698, #829, #876, #946, and #1424.

Usage:

@attrs.define
class Parent:
    a: int = 100
    
@attrs.define
class Child:
    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.validator
    def a_validator(...):
        ...

assert attrs.fields(Child).a.type is int
assert Child().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.define
class A:
    a: int = 1
    
@attrs.define
class B:
    b: int = 2
    
@attrs.define
class Composite:
    a = attrs.fields(A).a.evolve(...).to_field()
    b = attrs.fields(B).b.evolve(...).to_field()

Utility methods like attrs.make_class() and the these kwarg in attrs.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.s
class BaseClass:
    x: int = attr.ib(default=1)
    y: int = attr.ib(default=2)

@attr.s
class SubClass(BaseClass):
    x = attr.fields(BaseClass).x.evolve(default=3).to_field()

# Because x was redefined, it appears after y
assert "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 argument inherited to field, which tells attrs to use the ordering of the field in the parent class as opposed to adding to the end:

@attr.s
class SubClass(BaseClass):
    x = attr.fields(BaseClass).x.evolve(default=3, inherited=True).to_field()

assert "SubClass(x=3, y=2)" == repr(SubClass())

Criticisms of this inherited 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

  • Do not open pull requests from your main branch – use a separate branch!
  • Added tests for changed code.
  • New features have been added to our Hypothesis testing strategy.
  • Changes or additions to public APIs are reflected in our type stubs (files ending in .pyi).
    • ...and used in the stub test file tests/typing_example.py.
    • If they've been added to attr/__init__.pyi, they've also been re-imported in attrs/__init__.pyi.
  • Updated documentation for changed code.
    • New functions/classes have to be added to docs/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 appropriate versionadded, versionchanged, or deprecated directives.
      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 both attrs.define() and attr.s(), you have to add version directives to both.
  • Documentation in .rst and .md files is written using semantic newlines.
  • Changes (and possible deprecations) have news fragments in changelog.d.
  • Consider granting push permissions to the PR branch, so maintainers can fix minor issues themselves without pestering you.

redruin1 and others added 2 commits May 2, 2025 13:02
with additional `inherited` kwarg to preserve inherited class order
@Tinche
Copy link
Member

Tinche commented May 2, 2025

Why does it need to be attrs.fields(Parent).a.evolve(default=200).to_field() instead of maybe attrs.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

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