-
-
Notifications
You must be signed in to change notification settings - Fork 372
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
Suggestion: Overriding class-level kw_only=True
option in attributes
#980
Comments
That seems like the common semantics for other parameters, where do you expect things to break backwards-compatibility? |
I agree it's common semantics for other parameters, but the current state is that Here's a hypothetical example: @attrs.define
class Base:
a: int = attrs.field()
b: int = attrs.field(kw_only=False)
c: int = attrs.field(kw_only=True)
@attrs.define(kw_only=True)
class Derived1(Base):
pass
@attrs.define(kw_only=False)
class Derived2(Base):
pass
Although no one would write code like this, it illustrates a case where the behavior differs if we change the semantics of Of course, I support making the change since I think it's more intuitive and could reduce boilerplate code for some of my projects, but at the same time I realize that attrs has a backwards-compatible guarantee and I don't want to break that. I'm just wondering what's the best thing to do here. |
How about – and I suspect that's how it's handled elsewhere – we bake the In your example |
Right, that's how I would change it as well. I actually had a patch locally that looks like this — a very simple change: diff --git a/src/attr/_make.py b/src/attr/_make.py
index 4d1afe3..6faf525 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -109,7 +109,7 @@ def attrib(
type=None,
converter=None,
factory=None,
- kw_only=False,
+ kw_only=None,
eq=None,
order=None,
on_setattr=None,
@@ -561,9 +561,10 @@ def _transform_attrs(
cls, {a.name for a in own_attrs}
)
- if kw_only:
- own_attrs = [a.evolve(kw_only=True) for a in own_attrs]
- base_attrs = [a.evolve(kw_only=True) for a in base_attrs]
+ own_attrs = [
+ a.evolve(kw_only=kw_only) if a.kw_only is None else a
+ for a in own_attrs
+ ]
attrs = base_attrs + own_attrs To reiterate, I think it's totally possible (and quite easy) to allow such behavior, and I think such behavior is more intuitive than the current state. However, as my previous example, the change would be backwards-incompatible in certain cases, and I was looking for guidance on how to handle that. |
Sorry for being obtuse, but how is your example backwards-incompatible? If you bake your |
No worries! Sorry for not explaining this properly. You're right that in my example, Lines 542 to 544 in c860e9d
In the current implementation, if a subclass sets Let me know if you have questions, or maybe if I'm missing something! |
Sorry for the delay, I had to get structlog out of the door and I'm bad at deep multi-tasking. 🙈 I think I'm starting to sense what you consider breaking here: that if someone set Of course there's Hyrum's law, but We had 100 million downloads per month less back then, but IIRC, we've always did the same move in this case: switch defaults to Have you tried your changes (haven't looked too closely at them tho) and checked if our test suite still passes? 🤔 |
No worries!
So the following is the patch I applied on top of the current master: diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi
index 7aff65c..ea2ad21 100644
--- a/src/attr/__init__.pyi
+++ b/src/attr/__init__.pyi
@@ -174,7 +174,7 @@ def attrib(
type: None = ...,
converter: None = ...,
factory: None = ...,
- kw_only: bool = ...,
+ kw_only: Optional[bool] = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
@@ -195,7 +195,7 @@ def attrib(
type: Optional[Type[_T]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
- kw_only: bool = ...,
+ kw_only: Optional[bool] = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
@@ -215,7 +215,7 @@ def attrib(
type: Optional[Type[_T]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
- kw_only: bool = ...,
+ kw_only: Optional[bool] = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
@@ -235,7 +235,7 @@ def attrib(
type: object = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
- kw_only: bool = ...,
+ kw_only: Optional[bool] = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
@@ -252,7 +252,7 @@ def field(
metadata: Optional[Mapping[Any, Any]] = ...,
converter: None = ...,
factory: None = ...,
- kw_only: bool = ...,
+ kw_only: Optional[bool] = ...,
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
@@ -272,7 +272,7 @@ def field(
metadata: Optional[Mapping[Any, Any]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
- kw_only: bool = ...,
+ kw_only: Optional[bool] = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
@@ -291,7 +291,7 @@ def field(
metadata: Optional[Mapping[Any, Any]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
- kw_only: bool = ...,
+ kw_only: Optional[bool] = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
@@ -310,7 +310,7 @@ def field(
metadata: Optional[Mapping[Any, Any]] = ...,
converter: Optional[_ConverterType] = ...,
factory: Optional[Callable[[], _T]] = ...,
- kw_only: bool = ...,
+ kw_only: Optional[bool] = ...,
eq: Optional[_EqOrderType] = ...,
order: Optional[_EqOrderType] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
diff --git a/src/attr/_make.py b/src/attr/_make.py
index 4e6846a..6ee2914 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -97,7 +97,7 @@ def attrib(
type=None,
converter=None,
factory=None,
- kw_only=False,
+ kw_only=None,
eq=None,
order=None,
on_setattr=None,
@@ -545,9 +545,10 @@ def _transform_attrs(
cls, {a.name for a in own_attrs}
)
- if kw_only:
- own_attrs = [a.evolve(kw_only=True) for a in own_attrs]
- base_attrs = [a.evolve(kw_only=True) for a in base_attrs]
+ own_attrs = [
+ a.evolve(kw_only=kw_only) if a.kw_only is None else a
+ for a in own_attrs
+ ]
attrs = base_attrs + own_attrs
diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py
index 79e8a44..7160e9c 100644
--- a/src/attr/_next_gen.py
+++ b/src/attr/_next_gen.py
@@ -165,7 +165,7 @@ def field(
metadata=None,
converter=None,
factory=None,
- kw_only=False,
+ kw_only=None,
eq=None,
order=None,
on_setattr=None, All but one test pass. The failing test is I'm actually not sure what's the better behavior, with respect to base classes. If a base class
What I implemented is option 1, just because it's easiest to implement. Of the other two options, option 2 makes more sense to me, while option 3 sounds unnecessarily complicated. Curious to know what you think.
So I think that the only case where this would be backwards-incompatible is if someone has a |
First of all, thank you for building such a wonderful package! It is now an essential part of both my work and my hobby projects.
This issue is similar to #481, but I think it's more suitable to open a new issue for discussion instead of reviving a 4-year-old thread.
kw_only
can be set both on the class decorator and on attributes. Whenkw_only=False
on the class (this is the default), one can setkw_only=True
on attributes to make only those attributes keyword-only in the constructor. However, whenkw_only=True
on the class, all attributes (including those defined in base classes) become keyword-only, and settingkw_only=False
on attributes will not work.The following code snippet implements the logic described above:
attrs/src/attr/_make.py
Lines 564 to 566 in 7091b1f
In my opinion, this is a little confusing, since many of the other options that exist on both the class and attributes do give you a way to opt-out for a subset of attributes. For example,
eq
(and friends),repr
, andinit
all allow you to exclude an attribute from the auto-generated functions. It feels natural to assume the same forkw_only
; I don't have exact stats, but I know quite a few coworkers who were surprised by the current behavior.It would also be useful to allow overriding. For example, I have a class whose ideal constructor signature should look like this:
where
x
andy
are required, and all the rest are optional with defaults, usually some obscure setting that can be tweaked but are rarely tweaked. To implement such an interface, I would have to setkw_only=False
on the class, andkw_only=True
on most of the attributes. To make matters worse, I need to do the same for all new attributes in all of its subclasses as well. It would be beneficial if we could just setkw_only=True
on the class, and override a few attributes withkw_only=False
.If we are to allow overriding
kw_only
on attributes, this is how I imagine it should behave:kw_only
defaults to False, as it does today.kw_only
defaults to None. If it is set to True or False, then it stays kw-only or non-kw-only regardless of the class's setting; if it is not set (None), then it reflects the class'skw_only
value.kw_only
-ness of its base attributes.I'm open for debate on the semantics and I'd love input on how this can be improved or made more natural or useful.
Now, I would love to put up a pull request for this change, but I realize that this would break backwards-compability, so I'd like to seek permission from maintainers before I go ahead and do this. I'm also open to introducing a new class-level option (e.g.
kw_only_overridable
, or something less horrendous) that's disabled by default, similar to howcollect_by_mro
works. Please let me know if you think this proposal makes sense, and how we should move forward. Thanks!The text was updated successfully, but these errors were encountered: