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 <[email protected]> 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 <[email protected]> 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 -- [email protected]
> To unsubscribe send an email to [email protected]
> https://mail.python.org/mailman3/lists/python-ideas.python.org/
> Message archived at
> https://mail.python.org/archives/list/[email protected]/message/6L4W5OB23FBWZ7EZYDNCYSGT2CUAKYSX/
> Code of Conduct: http://python.org/psf/codeofconduct/
>
>
> _______________________________________________
> Python-ideas mailing list -- [email protected]
> To unsubscribe send an email to [email protected]
> https://mail.python.org/mailman3/lists/python-ideas.python.org/
> Message archived at
> https://mail.python.org/archives/list/[email protected]/message/VPSE34Z35XOXGFJMGTMLWDAMF7JKJYOJ/
> Code of Conduct: http://python.org/psf/codeofconduct/
>
> _______________________________________________
> Python-ideas mailing list -- [email protected]
> To unsubscribe send an email to [email protected]
> https://mail.python.org/mailman3/lists/python-ideas.python.org/
> Message archived at
> https://mail.python.org/archives/list/[email protected]/message/WBL4X46QG2HY5ZQWYVX4MXG5LK7QXBWB/
> Code of Conduct: http://python.org/psf/codeofconduct/
>
>
>
_______________________________________________
Python-ideas mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at
https://mail.python.org/archives/list/[email protected]/message/2LCC7M6XSCQMU2ZKJ63DRI2KLLB7TXAX/
Code of Conduct: http://python.org/psf/codeofconduct/