Just off the top of my head, a context manager like this would give access to the required local scope, if inspecting the execution frames isn't considered too hacky. class LocalsProcessor: def __enter__(self): self.locals_ref = inspect.currentframe().f_back.f_locals self.locals_prev = copy.deepcopy(self.locals_ref) # deep copy to ensure we also get a copy of the # __annotations__ def __exit__(self, exception_type, exception_value, traceback): ... # modify self.locals_ref based on the difference between # self.locals_ref and self.locals_prev and their # respective __annotations__ An issue I can see with this approach is that the context manager can only work off the difference between locals() before and after its scope, so it would ignore a duplicate assignment to the same value as before, for a name that existed before the context manager entered, in a way that might be unexpected: class Example: a: str b: int with LocalsProcessor(): a: bool # we can detect that 'a' changed and what it changed to/from # because its value in the __annotations__ dict is different b: int # there is no way to detect that 'b' was redeclared within # the scope of the contextmanager because it has the same # annotation before and after Granted, I have no idea who would ever write code like this (!) but I thought I'd mention that as a problematic edge-case. Maybe there's a better way to approach this that I can't think of. Or maybe it's possible that using context managers for this isn't realistic because of implementation issues that just can't be resolved. I just really like the semantics of it :) On Thu, Mar 11, 2021 at 11:08 PM Paul Bryan <pbryan@anode.ca> wrote:
The syntax of what you're proposing seems fairly intuitive (they might not even need to be callable). I'm struggling to envision how the context manager would acquire scope to mark fields up in the (not yet defined) dataclass, and to capture the attributes that are being defined within.
On Thu, 2021-03-11 at 22:53 +0000, Matt del Valle wrote:
Disclaimer: I posted this earlier today but I think due to some first-post moderation related issues (that I've hopefully now gotten sorted out!) it may not have gone through. I'm posting this again just in case. If it's gone through and you've already seen it then I'm super sorry, please just ignore this.
If something like what you're suggesting were to be implemented I would much rather it be done with context managers than position-dependent special values, because otherwise you once again end up in a situation where it's impossible to easily subclass a dataclass (which was one of the primary reasons this conversation even got started in the first place). So, for example:
import dataclasses
@dataclasses.dataclass class SomeClass: c: bool = False # a normal field with a default value does not # prevent subsequent positional fields from # having no default value (such as 'a' below) # however, all further normal fields now must # specify a default value (such as 'd' below)
with dataclasses.positional(): a: int b: float = 3.14 # once a positional field with a default value shows up # all further positional fields and ALL normal fields # (even retroactively!) must also specify defaults # (for example, field 'c' above is # now forced to specify a default value)
with dataclasses.keyword(): e: list f: set = dataclasses.field(default_factory=set) # once a keyword field with a default value shows up # all further keyword fields must also specify defaults
d: dict = dataclasses.field(default_factory=dict) # This ordering is clearly insane, but the essential # point is that it works even with weird ordering # which is necessary for it to work when subclassing # where the order will almost always be wonky # # A sane version of the above would be:
@dataclasses.dataclass class SomeClass: with dataclasses.positional(): a: int b: float = 3.14
c: bool = False d: dict = dataclasses.field(default_factory=dict)
with dataclasses.keyword(): e: list f: set = dataclasses.field(default_factory=set)
# either of the above will generate an __init__ like: def __init__(self, a: int, b: float = 3.14, /, c: bool = False, d: dict = None, *, e: list, f: set = None): self.a = a self.b = b self.c = c self.d = dict() if d is None else d self.e = e self.f = set() if f is None else f # parameters are arranged in order as # positional -> normal -> keyword # within the order they were defined in each # individual category, but not necessarily # whatever order they were defined in overall # # This is subclass-friendly! # # it should hopefully be obvious that we could # have cut this class in half at literally any # point (as long as the the parent class has # the earlier arguments within each category) # and put the rest into a child class and # it would still have worked and generated the # same __init__ signature # # For example:
@dataclasses.dataclass class Parent: with dataclasses.positional(): a: int
c: bool = False
with dataclasses.keyword(): e: list
@dataclasses.dataclass class Child(Parent): with dataclasses.positional(): b: float = 3.14
d: dict = dataclasses.field(default_factory=dict)
with dataclasses.keyword(): f: set = dataclasses.field(default_factory=set) # Child now has the same __init__ signature as # SomeClass above
(In case the above code doesn't render properly on your screen, I've uploaded it to GitHub at: https://github.com/matthewgdv/dataclass_arg_contextmanager/blob/main/example... )
Honestly, the more I think about it, the more I love the idea of something like this (even if it's not *exactly* the same as my suggestion). Right now dataclasses do not support the full range of __init__ signatures you could generate with a normal class, and they are extremely hostile to subclassing. That is a failing that often forces people to fall back to normal classes in otherwise ideal dataclass use-case situations.
On Thu, Mar 11, 2021 at 10:15 PM Paul Bryan <pbryan@anode.ca> wrote:
If you're proposing something like this, then I think it would be compatible:
class Hmm:
#
this: int
that: float
#
pos: PosOnly
#
these: str
those: str
#
key: KWOnly
#
some: list
On Thu, 2021-03-11 at 14:06 -0800, Ethan Furman wrote:
On 3/11/21 10:50 AM, Paul Bryan wrote:
On Thu, 2021-03-11 at 10:45 -0800, Ethan Furman wrote:
On 3/10/21 9:47 PM, Eric V. Smith wrote:
I'm not sure of the best way to achieve this. Using flags to field() doesn't sound awesome, but could be made to work. Or maybe special field names or types? I'm not crazy about that, but using special types would let you do something like:
@dataclasses.dataclass class Point: x: int = 0 _: dataclasses.KEYWORD_ONLY y: int z: int t: int = 0
Maybe something like this?
class Hmm: # this: int that: float # pos: '/' # these: str those: str # key: '*' # some: list
>>> Hmm.__dict__['__annotations__'] { 'this': <class 'int'>, 'that': <class 'float'>, 'pos': '/', 'these': <class 'str'>, 'those': <class 'str'>, 'key': '*', 'some': <class 'list'>, }
The name of 'pos' and 'key' can be convention, since the actual name is irrelevant. They do have to be unique, though. ;-)
It's current convention (and is used by typing module and static type checkers) that string annotations evaluate to valid Python types.
So make '/' and '*' be imports from dataclasses:
from dataclasses import dataclass, PosOnly, KWOnly
-- ~Ethan~ _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/6L4W5O... Code of Conduct: http://python.org/psf/codeofconduct/
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/VPSE34... Code of Conduct: http://python.org/psf/codeofconduct/
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/WBL4X4... Code of Conduct: http://python.org/psf/codeofconduct/