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

Generic type not handled correctly in dataclass attribute with converter #9512

Closed
erictraut opened this issue Nov 28, 2024 · 5 comments
Closed
Labels
addressed in next version Issue is fixed and will appear in next published version bug Something isn't working

Comments

@erictraut
Copy link
Collaborator

This bug report comes by way of this post from @Darkdragon84.

from typing import Generic, TypeVar

from attrs import field, frozen
from typing_extensions import reveal_type

T = TypeVar("T")


@frozen
class Base(Generic[T]):
    data: set[T] = field()


@frozen
class BaseConverted(Generic[T]):
    data: set[T] = field(converter=set)


reveal_type(Base({1, 2}))  # Type of "Base(1, 2)" is "Base[int]"
reveal_type(Base({1, 2}).data)  # Type of "Base(1, 2).data" is "set[int]"
reveal_type(BaseConverted([1, 2]))  # Type of "BaseConverted([1, 2])" is "BaseConverted[int]"
reveal_type(BaseConverted([1, 2]).data)  # Type of "BaseConverted([1, 2]).data" is "set[T@BaseConverted]"
@erictraut erictraut added the bug Something isn't working label Nov 28, 2024
@CrzyHAX91
Copy link

Yo dit al geprobeerd?

De verschillen in de types die worden onthuld door reveal_type kunnen worden verklaard door hoe attrs en het converter-argument werken in combinatie met type-inferentie in Python.

Probleemanalyse:
Base zonder converter:

In de definitie van Base, wordt data: set[T] rechtstreeks toegewezen. Python (en attrs) kan type-informatie direct afleiden uit de initiële waarde die wordt meegegeven tijdens het instantiëren. Hierdoor kan Base({1, 2}) correct worden geïnterpreteerd als Base[int], en hetzelfde geldt voor Base({1, 2}).data.
BaseConverted met converter:

Bij BaseConverted, wordt de data-waarde verwerkt door field(converter=set), wat betekent dat alle invoer (bijvoorbeeld een lijst [1, 2]) wordt omgezet naar een set door de converterfunctie.
Deze extra stap introduceert een nuance in type-inferentie:
BaseConverted([1, 2]) wordt geïnterpreteerd als BaseConverted[int], omdat de invoer [1, 2] duidelijk elementen van type int bevat.
Maar BaseConverted([1, 2]).data wordt geïnterpreteerd als set[T@BaseConverted] omdat de converter het daadwerkelijke type van data loskoppelt van de initiële invoer. Python kan de exacte concrete typebinding (T) niet meer direct afleiden.
Gedetailleerd verschil:
Waarom set[T] voor Base:
Base({1, 2}) heeft een direct type (set[int]) dat kan worden gebruikt om T in Base[T] te bepalen. Hierdoor wordt Base({1, 2}).data ook correct geïnterpreteerd als set[int].
Waarom set[T@BaseConverted] voor BaseConverted:
De converter wijzigt de invoer ([1, 2] wordt omgezet naar {1, 2}) voordat deze aan data wordt toegewezen. Dit maakt het moeilijk voor Python om het specifieke type van T op dezelfde manier te achterhalen, waardoor het resultaat meer generiek blijft als set[T@BaseConverted].
Oplossing of verduidelijking:
Als je wilt dat BaseConverted hetzelfde gedrag heeft als Base, kun je de converter meer expliciet maken door het type te annoteren of te casten. Bijvoorbeeld:

python
Code kopiëren
from typing import cast

@Frozen
class BaseConverted(Generic[T]):
data: set[T] = field(converter=lambda x: cast(set[T], set(x)))
Dit dwingt data expliciet om het type set[T] te hebben, en reveal_type zou dan ook correcter gedrag moeten tonen.

Samenvatting:
Het gebruik van een converter in combinatie met generieke types kan type-inferentie bemoeilijken, omdat de converter een extra abstractielaag introduceert. Zonder expliciete type-aanduiding valt Python terug op een generieke interpretatie (set[T@BaseConverted] in dit geval).

@loing-cdut

This comment has been minimized.

@Darkdragon84
Copy link

Hi! Thanks for the replies @CrzyHAX91 @loing-cdut .

No, using a lambda or any other properly annotated function doesn't work for me either (and for the record, set IS a function too and should have proper type annotations at least from type_shed, right?):

from collections.abc import Iterable
from typing import Generic, TypeVar

from attrs import field, frozen
from typing_extensions import reveal_type

T = TypeVar("T")


@frozen
class Base(Generic[T]):
    data: set[T] = field()


def to_set(data: Iterable[T]) -> set[T]:
    return set(data)


@frozen
class BaseConverted(Generic[T]):
    data: set[T] = field(converter=to_set)


reveal_type(Base({1, 2}))  # Type of "Base(1, 2)" is "Base[int]"
reveal_type(Base({1, 2}).data)  # Type of "Base(1, 2).data" is "set[int]"
reveal_type(BaseConverted([1, 2]))  # Type of "BaseConverted([1, 2])" is "BaseConverted[int]"
reveal_type(BaseConverted([1, 2]).data)  # Type of "BaseConverted([1, 2]).data" is "set[T@BaseConverted]"

In fact, the lambda version even leads to a regression:

@frozen
class BaseConverted(Generic[T]):
    data: set[T] = field(converter=lambda x: set(x))

reveal_type(BaseConverted([1, 2]))  # Type of "BaseConverted([1, 2])" is "BaseConverted[Unknown]"
reveal_type(BaseConverted([1, 2]).data)  # Type of "BaseConverted([1, 2]).data" is "set[T@BaseConverted]"

so, pyright can't even figure out T for BaseConverted itself.

Have you tried your modified version?

For the record: I'm on

  • Python 3.11.9
  • attrs 24.2.0
  • pyright 1.1.389

@erictraut
Copy link
Collaborator Author

This is addressed in pyright 1.1.390.

@Darkdragon84
Copy link

Wow, fantastic, thanks so much for the quick fix 👏 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addressed in next version Issue is fixed and will appear in next published version bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants