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

Nested tagged unions stopped working #10764

Closed
levrik opened this issue Jul 5, 2021 · 4 comments
Closed

Nested tagged unions stopped working #10764

levrik opened this issue Jul 5, 2021 · 4 comments
Labels
bug mypy got something wrong

Comments

@levrik
Copy link

levrik commented Jul 5, 2021

Bug Report

Starting with mypy 0.900 tagged unions don't work anymore if they are nested.

To Reproduce

from typing import Literal, Union
from enum import Enum

class ModelType(str, Enum):
    Simple1 = "simple1"
    Simple2 = "simple2"
    Complex1 = "complex1"
    Complex2 = "complex2"

class BaseModel:
    type: ModelType
    
class BaseSimpleModel(BaseModel):
    type: Literal[ModelType.Simple1, ModelType.Simple2]
    simple: str
    
class Simple1Model(BaseSimpleModel):
    type: Literal[ModelType.Simple1]
    
class Simple2Model(BaseSimpleModel):
    type: Literal[ModelType.Simple2]
    
SimpleModel = Union[Simple1Model, Simple2Model]

class BaseComplexModel(BaseModel):
    type: Literal[ModelType.Complex1, ModelType.Complex2]
    complex: str

class Complex1Model(BaseComplexModel):
    type: Literal[ModelType.Complex1]
    
class Complex2Model(BaseComplexModel):
    type: Literal[ModelType.Complex2]
    
ComplexModel = Union[Complex1Model, Complex2Model]
    
Model = Union[SimpleModel, ComplexModel]


model: Model = Simple1Model()

if model.type == ModelType.Simple1 or model.type == ModelType.Simple2:
    reveal_type(model)
    reveal_type(model.simple)
else:
    reveal_type(model)
    reveal_type(model.complex)

Type-check with mypy >= 0.900 and it won't pass, try with mypy 0.812 for example, and it will pass.

I've also prepared it on mypy-play.

Working (mypy 0.812):
https://mypy-play.net/?mypy=0.812&python=3.9&gist=b15073e2bdc681689f2e6f9bc8556ead
Broken (mypy 0.910):
https://mypy-play.net/?mypy=0.910&python=3.9&gist=b15073e2bdc681689f2e6f9bc8556ead

Expected Behavior

main.py:43: note: Revealed type is 'Union[main.Simple1Model, main.Simple2Model]'
main.py:44: note: Revealed type is 'builtins.str'
main.py:46: note: Revealed type is 'Union[main.Complex1Model, main.Complex2Model]'
main.py:47: note: Revealed type is 'builtins.str'

Actual Behavior

main.py:43: note: Revealed type is "Union[main.Simple1Model, main.Simple2Model, main.Complex1Model, main.Complex2Model]"
main.py:44: error: Item "Complex1Model" of "Union[Union[Simple1Model, Simple2Model], Union[Complex1Model, Complex2Model]]" has no attribute "simple"
main.py:44: error: Item "Complex2Model" of "Union[Union[Simple1Model, Simple2Model], Union[Complex1Model, Complex2Model]]" has no attribute "simple"
main.py:44: note: Revealed type is "Union[builtins.str, Any]"
main.py:46: note: Revealed type is "Union[main.Simple1Model, main.Simple2Model, main.Complex1Model, main.Complex2Model]"
main.py:47: error: Item "Simple1Model" of "Union[Union[Simple1Model, Simple2Model], Union[Complex1Model, Complex2Model]]" has no attribute "complex"
main.py:47: error: Item "Simple2Model" of "Union[Union[Simple1Model, Simple2Model], Union[Complex1Model, Complex2Model]]" has no attribute "complex"
main.py:47: note: Revealed type is "Union[Any, builtins.str]"

As you can see mypy isn't able to correctly nail down the union members anymore when unions are nested.

Your Environment

  • Mypy version used: 0.912
  • Mypy command-line flags: None
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.9
  • Operating system and version: macOS
@levrik levrik added the bug mypy got something wrong label Jul 5, 2021
@levrik levrik changed the title Recursive tagged unions stopped working Nested tagged unions stopped working Jul 5, 2021
@hauntsaninja
Copy link
Collaborator

Thanks for following up!
Using mypy_primer -p test.py --bisect-output 'has no attribute' --old v0.812 --debug I bisected this to #9097 (cc @ethan-leba in case you have time to take a look)

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Jul 5, 2021

Not obvious and not fully equivalent code, but if you change your check to: if model.type is ModelType.Simple1 or model.type is ModelType.Simple2: you'll pass type checking.

(which is similar behaviour to #6366 (comment) , see other comments on that issue as well)

@levrik
Copy link
Author

levrik commented Jul 6, 2021

@hauntsaninja Thanks a lot for pointing me to this workaround.

But I've noticed another issue demonstrated here:

from typing import Literal, Union
from enum import Enum

class ModelType(str, Enum):
    Simple1 = "simple1"
    Simple2 = "simple2"
    Complex1 = "complex1"
    Complex2 = "complex2"

class BaseModel:
    type: ModelType
    
class BaseSimpleModel(BaseModel):
    type: Literal[ModelType.Simple1, ModelType.Simple2]
    simple: str
    
class Simple1Model(BaseSimpleModel):
    type: Literal[ModelType.Simple1]
    
class Simple2Model(BaseSimpleModel):
    type: Literal[ModelType.Simple2]
    
SimpleModel = Union[Simple1Model, Simple2Model]

class BaseComplexModel(BaseModel):
    type: Literal[ModelType.Complex1, ModelType.Complex2]
    complex: str

class Complex1Model(BaseComplexModel):
    type: Literal[ModelType.Complex1]
    
class Complex2Model(BaseComplexModel):
    type: Literal[ModelType.Complex2]
    
ComplexModel = Union[Complex1Model, Complex2Model]
    
Model = Union[SimpleModel, ComplexModel]


model: Model = Simple1Model()

if model.type is ModelType.Simple1:
    reveal_type(model)

Mypy play link: https://mypy-play.net/?mypy=0.910&python=3.9&gist=3fea5f8e3bfa94c7b9835fb53133af4d

Expected output:

main.py:43: note: Revealed type is 'main.Simple1Model'

Actual output:

main.py:43: note: Revealed type is 'Union[main.Simple1Model, main.Simple2Model]'

It seems to nail down the type just one level to the right nested union but not further down.

I'm not fully sure if this is connected to the particular error I was reporting here as this also happens in mypy 0.812.
Should I open a separate issue?

@hauntsaninja
Copy link
Collaborator

I think I fixed the original issue in #11521 (not yet released) and the issue in your second comment got fixed in #11204

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

No branches or pull requests

2 participants