Skip to content
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

Coercive __setattr__ support #6940

Closed
MasonMcGill opened this issue Jun 5, 2019 · 3 comments
Closed

Coercive __setattr__ support #6940

MasonMcGill opened this issue Jun 5, 2019 · 3 comments
Labels

Comments

@MasonMcGill
Copy link

MasonMcGill commented Jun 5, 2019

[Feature request / documentation enhancement proposal if it turns out this exists : ]

Hello, I'm looking for a way to support code like the following:

class Color:
    ''' [Many useful methods here] '''

class Palette:
    def __setattr__(self, key: str, value: object) -> None:
        ''' [Implicitly convert `value` to a `Color`.] '''

class SoftPastels(Palette):
    green: Color
    blue: Color

sp = SoftPastels()
sp.green = (127, 255, 127)  # -- "Incompatible types in assignment", MyPy 0.701
sp.blue = '#8888ff'         # -- "Incompatible types in assignment", MyPy 0.701

My use-case involves implicitly converting to numpy.ndarrays and/or h5py.Datasets, and I've seen this pattern (performing implicit conversion on attribute assignment) used to improve API ergonomics in a handful of other libraries (e.g. Pandas and the Blender API, IIRC).

Is there (or could there be) any way to type this?

@ethanhs
Copy link
Collaborator

ethanhs commented Jun 5, 2019

__setattr__ support was added so that you could assign to arbitrary attributes without needing to sprinkle # type: ignores everywhere, so I don't think it will do what you want.

I think you can accomplish what you want pretending that Color is a data descriptor protocol that coerces things. Something like:

from typing_extensions import Protocol
from typing import Any, Union, Tuple, Optional

class CoerciveDataDescriptor(Protocol):
    def __get__(self, o: Optional[object], ty: Optional[type]) -> Color:
        ...
    # n.b. if you want it to support more types, add to this union. Alternatively, you can always fall back to Any.
    def __set__(self, o: Optional[object], val: Union[Tuple[int, int, int], str, Color]) -> None:
        ...

class Color(CoerciveDataDescriptor):
    ...

class Palette:
    ...

class SoftPastels(Palette):
    green: Color
    blue: Color
    red: Color
    no: Color

sp = SoftPastels()
sp.green = (127, 255, 127)  # ok
sp.blue = '#8888ff' # ok
sp.red = Color() # ok
sp.no = 1  # error: Incompatible types in assignment (expression has type "int", variable has type "Union[Tuple[int, int, int], str, Color]")
reveal_type(sp.green)  # error: Revealed type is 'main.Color'
reveal_type(sp.blue)  # error: Revealed type is 'main.Color'
reveal_type(sp.red)  # error: Revealed type is 'main.Color'

This doesn't exactly mirror the semantics of what you want, however, since it moves the type coercion into the Color class.

If this is such a common pattern perhaps we should think of ways to make it easier, as a data descriptor protocol is not obvious (and not exactly correct).

@MasonMcGill
Copy link
Author

Ah! I had no idea MyPy supported descriptors like this; this is wonderful.

Though, why do you say it's not exactly correct? Do you just mean that requiring the annotation type (Color) to be a descriptor protocol could be an inflexible typing interface for library authors, or is there some deeper incompatibility between data descriptors and protocols in this context?

It looks like the relevant documentation issue is #2566. Maybe this should be marked as a duplicate?

@ethanhs
Copy link
Collaborator

ethanhs commented Jun 5, 2019

It isn't exactly what you requested, which is a way to say "all attributes of this class are coerced when assigned from type T into type S". I gave a solution "All attributes annotated as type S will be coerced from type T when assigned to". Though I would argue the first is cleaner and more desirable :)

I agree this can be closed, since your question is answered, and we need to document descriptors according to #2566.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants