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

Attribute always has first type in Union [bug] #2079

Closed
3 tasks done
InnovativeInventor opened this issue Nov 1, 2020 · 9 comments
Closed
3 tasks done

Attribute always has first type in Union [bug] #2079

InnovativeInventor opened this issue Nov 1, 2020 · 9 comments
Labels
bug V1 Bug related to Pydantic V1.X

Comments

@InnovativeInventor
Copy link

InnovativeInventor commented Nov 1, 2020

Checks

  • I added a descriptive title to this issue
  • I have searched (google, github) for similar issues and couldn't find anything
  • I have read and followed the docs and still think this is a bug

Bug

Output of python -c "import pydantic.utils; print(pydantic.utils.version_info())":

             pydantic version: 1.7.1
            pydantic compiled: True
                 install path: /home/max/.pyenv/versions/3.8.5/lib/python3.8/site-packages/pydantic
               python version: 3.8.5 (default, Aug  7 2020, 10:30:17)  [GCC 9.3.0]
                     platform: Linux-5.4.0-7634-generic-x86_64-with-glibc2.29
     optional deps. installed: ['typing-extensions']

Reproduction steps

from pydantic import BaseModel
from typing import Union

class Foo(BaseModel):
    pass
class Bar(BaseModel):
    pass
class Test(BaseModel):
    test: Union[Foo, Bar]

print("Should be Foo:", Test(test=Foo()))
print("Should be Bar:", Test(test=Bar()))
print("Most certainly should be Bar:", type(Test(test=Bar()).test))

Result:

Should be Foo: test=Foo()
Should be Bar: test=Foo()
Most certainly should be Bar: <class '__main__.Foo'>
@InnovativeInventor InnovativeInventor added the bug V1 Bug related to Pydantic V1.X label Nov 1, 2020
@PrettyWood
Copy link
Member

Hello @InnovativeInventor
This is a known issue (see #619 or #1423), which I'm planning to work on (for v1.8 hopefully).
Have a good day

@InnovativeInventor
Copy link
Author

@PrettyWood thanks for letting me know! Keep up the good work in this project!

Also, just to double check, even when accessing the attribute, I get:

print("Most certainly should be Bar:", type(Test(test=Bar()).test))

to be:

Most certainly should be Bar: <class '__main__.Foo'>

Perhaps I missed this, but I couldn't find an example of this happening in the issues that you linked (although I think the problems they describe are mostly the same as the issue that I reported).

@InnovativeInventor
Copy link
Author

Also, here are a few more examples:

print("Should be true:", isinstance(Test(test=Bar()).test, Bar))
print("Should be false:", isinstance(Test(test=Bar()).test, Foo))

Output:

Should be true: False
Should be false: True

@PrettyWood
Copy link
Member

Hey @InnovativeInventor
Yes it's because the order of the union is Union[Foo, Bar] so it first tries to coerce it to Foo and succeeds.
So you end up with a Foo instance. If you change the order of the union Union[Bar, Foo] you'll get the opposite.

@InnovativeInventor
Copy link
Author

Awesome, thanks for clarifying!

@PrettyWood
Copy link
Member

PrettyWood commented Nov 1, 2020

I've done a quick POC of what could be done to make it work
I used typingx library to have an isinstance function that supports generic types

from types import new_class
from typing import *

from typingx import isinstancex
from pydantic import BaseModel

T = TypeVar("T")


def _display_type(v: Any) -> str:
    try:
        return v.__name__
    except AttributeError:
        # happens with typing objects
        return str(v).replace("typing.", "")


class Strict(Generic[T]):
    __typelike__: T

    @classmethod
    def __class_getitem__(cls, typelike: T) -> T:
        new_cls = new_class(
            f"Strict[{_display_type(typelike)}]",
            (cls,),
            {},
            lambda ns: ns.update({"__typelike__": typelike}),
        )
        return cast(T, new_cls)

    @classmethod
    def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]:
        yield cls.validate

    @classmethod
    def validate(cls, value: Any) -> T:
        if not isinstancex(value, cls.__typelike__):
            raise TypeError(f"{value!r} is not of valid type")
        return value


class Foo(BaseModel):
    pass


class Bar(BaseModel):
    pass


class Test(BaseModel):
    test: Strict[Union[Foo, Bar]]


class Pika(BaseModel):
    x: Strict[Union[Dict[str, str], List[Tuple[str, str]]]]


assert type(Test(test=Foo()).test) is Foo
assert type(Test(test=Bar()).test) is Bar
assert isinstance(Test(test=Bar()).test, Bar) is True
assert isinstance(Test(test=Bar()).test, Foo) is False
assert Pika(x={"a": "b"}).x == {"a": "b"}
assert Pika(x=[("a", "b")]).x == [("a", "b")]

Cheers

@InnovativeInventor
Copy link
Author

Wow @PrettyWood, thanks so much! Seriously thanks a lot!

@Pryanga
Copy link

Pryanga commented Mar 10, 2022

An easier solution will be set Config withing the Pydantic Model. Refere to this.

@PrettyWood
Copy link
Member

The easiest solution now is to set Config.smart_union to True, which will be the default in v2.
Note that this option does not support compound types yet so a solution like #2079 (comment) may still be useful in some cases

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug V1 Bug related to Pydantic V1.X
Projects
None yet
Development

No branches or pull requests

3 participants