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']
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'>
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
@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).
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
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.
Awesome, thanks for clarifying!
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
Wow @PrettyWood, thanks so much! Seriously thanks a lot!
Most helpful comment
I've done a quick POC of what could be done to make it work
I used typingx library to have an
isinstancefunction that supports generic typesCheers