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 <pbr...@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.py > ) > > 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 <pbr...@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-le...@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/6L4W5OB23FBWZ7EZYDNCYSGT2CUAKYSX/ > Code of Conduct: http://python.org/psf/codeofconduct/ > > > _______________________________________________ > Python-ideas mailing list -- python-ideas@python.org > To unsubscribe send an email to python-ideas-le...@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/VPSE34Z35XOXGFJMGTMLWDAMF7JKJYOJ/ > Code of Conduct: http://python.org/psf/codeofconduct/ > > _______________________________________________ > Python-ideas mailing list -- python-ideas@python.org > To unsubscribe send an email to python-ideas-le...@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/WBL4X46QG2HY5ZQWYVX4MXG5LK7QXBWB/ > Code of Conduct: http://python.org/psf/codeofconduct/ > > >
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-le...@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/2LCC7M6XSCQMU2ZKJ63DRI2KLLB7TXAX/ Code of Conduct: http://python.org/psf/codeofconduct/