-
-
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
Star-args attribute? #110
Comments
Currently, an init does not have The result would be the same for unexpected init parameters except it would be However cases like the above example would be handled, plus classes that have a need to store kwargs in dictionary would also be possible. import attr
@attr.s
class MyClass:
def __attrs_post_init__(self, *foobars):
self.foobars = foobars
MyClass(1, 2, 3).foobars # [1, 2, 3] I am new to attrs, so please forgive if I am missing something, but I have a specific need for the |
I envision something like this: @attr.s
class MyClass:
contents = attr.ib(all_args=True)
# all further attr.ibs must be keyword args because `contents` has consumed all positional args
description = attr.ib(default=None) This would be especially useful in our Conformity, which makes heavy use of Attrs but has to skip Attrs in certain places because we need to support star-args. |
So because writing Python decorators is my idea of fun, I made this proof of concept: @star_attrs(auto_attribs=True)
class Foo:
foo: str
args: Tuple[str, ...] = star_attrib()
bar: int = attrib(kw_only=True, factory=int)
kwargs: Mapping[str, int] = star_attrib(kw_only=True) Which can be used like this: >>> Foo?
Init signature: Foo(foo: str, *args: str, bar: int = NOTHING, **kwargs: int) -> None
...
>>> Foo("foo", "bar", "baz", bar=1, baz=2, qux=3)
Foo(foo='foo', args=('bar', 'baz'), bar=1, kwargs={'baz': 2, 'qux': 3}) Note the correct signature and types! Also the errors are sensible: >>> Foo(bar=2)
Traceback (most recent call last):
...
TypeError: __init__() missing 1 required positional argument: 'foo' Both Another question is how converters and validators should work. In my code nothing special happens: >>> @star_attrs
... class Foo:
... args = star_attrib(converter=set)
...
>>> Foo(1, 2, 3, 2)
Foo(args={1, 2, 3}) But an argument can be made for applying converters & friends individually. Yet another question is typing. Can we convert between “collected” argument type and init signature annotations? If you look at regular Python functions, it appears that
And these won't and produce
Since attrs strip annotations from Another small issue is, what to do given something like this: @star_attrs
class Foo:
args = star_attrib()
bar = attrib()
kwargs = star_attrib(kw_only=True) At first glance, it looks like the signature of this would be
A case can be made for adding This also works with subclasses without surprises. I'm not sure what to do if the type is something along P.S. It's not possible to typecheck this in a nice way, but a workaround is possible. P.P.S. it would be nice if the team decided on the API, then someone perhaps could write a PR~ |
I'd also like to see something like this: import attr
@attr.s
class MyClass:
ham = attr.ib()
spam = attr.ib()
_= attr.ib(init="/")
eggs = attr.ib()
_ = attr.ib(init="*")
bacon = attr.ib()
kwargs = attr.ib(init="**") would produce a class like: class MyClass:
def __init__(self, ham, spam, /, eggs, *, bacon, **kwargs): |
I've received an anonymous suggestion to add:
To allow |
this seems to be possible; this: class PrintingDict(dict):
def __setitem__(self, key, value):
print(f"setting {key} to {value}")
super().__setitem__(key, value)
class DefPrinter(type):
@classmethod
def __prepare__(metacls, name, bases, **kwargs):
return PrintingDict()
def __new__(metacls, name, bases, sad):
return super().__new__(metacls, name, bases, dict(sad))
def asterisk():
import inspect
frame = inspect.currentframe()
frame.f_back.f_locals["asterisk"] = True
def slash():
import inspect
frame = inspect.currentframe()
frame.f_back.f_locals["slash"] = True
class Foo(metaclass=DefPrinter):
a = attrib()
asterisk()
c = attrib()
slash()
d = attrib(kw_only=True) will print
but i would perhaps prefer something like... from attr import attr, attrs
from attr import positional_only, keyword_only
from star_attr import star_attrib
@attrs
class Foo:
with positional_only:
ham = attrib()
spam = attrib()
eggs = attrib()
with keyword_only:
bacon = attrib()
kwargs = star_attrib() this probably would also require |
The original issue is now easy enough and doesn't require extra syntax: In [1]: import attr
In [2]: @attr.define
...: class C:
...: foobars = attr.ib()
...:
...: def __init__(self, *args):
...: self.__attrs_init__(args)
...:
In [3]: C(1,2,3)
Out[3]: C(foobars=(1, 2, 3)) Writing a helper for that should be trivial. |
this has always been easy enough with classmethods: class Foo:
@classmethod
def from_list(cls, *list_of_data):
return cls(list_of_data) what advantage does this issue was about not having to write methods like this. also, a big point of star-arg-aware classes is that they can be subclassed without having having to override constructors, and i don't think |
Another use case where direct support for star-args and star-kwargs would be really elegant: from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence
from typing import AnyStr, Generic, TypeVar
from typing_extensions import ParamSpec, Concatenate as ParamConcat
_P = ParamSpec("_P")
_A = TypeVar("_A")
class LogConverter(Generic[_A]):
value: _A
converter: Callable[ParamConcat[_A, P], str]
converter_args: Sequence[object]
converter_kwargs: dict[str, object]
def __init__(
self,
value: _A,
converter: Callable[ParamConcat[_A, _P], str],
*converter_args: _P.args,
**converter_kwargs: _P.kwargs,
):
self.value = value
self.converter = converter
self.converter_args = converter_args
self.converter_kwargs = converter_kwargs
def __str__(self) -> str:
return self.converter(value, *self.converter_args, **self.converter_kwargs) A hypothetical Attrs version could be: from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence
from typing import AnyStr, Generic, TypeVar
from typing_extensions import ParamSpec, Concatenate as ParamConcat
_P = ParamSpec("_P")
_A = TypeVar("_A")
import attr
@attr.define
class LogConverter(Generic[_A]):
value: _A
converter: Callable[ParamConcat[_A, _P], str]
converter_args: Sequence[object] = attr.variadic.args()
converter_kwargs: dict[str, object] = attr.variadic.kwargs()
def __str__(self) -> str:
return self.converter(value, *self.converter_args, **self.converter_kwargs) There is actually downside here, in that you lose the ability to annotate the |
I don't see a way to have this right now, but it'd be great to be able to make a particular attribute a star-args, something like:
The text was updated successfully, but these errors were encountered: