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

MappedAsDataclass Optional generates non-Optional typing annotations #9855

Closed
2 tasks done
BranislavBajuzik opened this issue May 29, 2023 · 6 comments
Closed
2 tasks done
Labels
dataclasses duplicate This issue or pull request already exists orm third party integration issues issues to do with other libraries and frameworks typing pep -484 typing issues. independent of "mypy"

Comments

@BranislavBajuzik
Copy link

Ensure stubs packages are not installed

  • No sqlalchemy stub packages is installed (both sqlalchemy-stubs and sqlalchemy2-stubs are not compatible with v2)

Verify if the api is typed

  • The api is not in a module listed in #6810 so it should pass type checking

Describe the typing issue

When having MappedAsDataclass in the class hierarchy, Mypy reports Optional attributes as not Optional.

The generated DDL is correct, without the NOT NULL part

To Reproduce

from sqlalchemy.orm import MappedAsDataclass, DeclarativeBase, Mapped


class Test(MappedAsDataclass, DeclarativeBase):
    __tablename__ = "test"
    test_str_attribute: Mapped[str | None]


Test(test_str_attribute=None)

Error

error: Argument "test_str_attribute" to "Test" has incompatible type "None"; expected "Union[SQLCoreOperations[Optional[str]], str]"  [arg-type]

Versions

  • OS: Kubuntu 22.04
  • Python: 3.10.6
  • SQLAlchemy: 2.0.15
  • Type checker: mypy 1.3.0 (compiled: yes)

Additional context

This seems oddly similar to #9567, but the referenced mypy issue, python/mypy#13856, is marked as resolved in 1.2 (also in docs)

Thank you

@BranislavBajuzik BranislavBajuzik added requires triage New issue that requires categorization typing pep -484 typing issues. independent of "mypy" labels May 29, 2023
@CaselIT
Copy link
Member

CaselIT commented May 29, 2023

Hi,

That's expected. If you try to run the above code it errors with:

sqlalchemy.exc.InvalidRequestError: Cannot use 'DeclarativeBase' directly as a declarative base class. Create a Base by creating a subclass of it.

Follow what the error suggests and it should work

@CaselIT CaselIT closed this as not planned Won't fix, can't repro, duplicate, stale May 29, 2023
@CaselIT CaselIT added expected behavior that's how it's meant to work. consider the "documentation" label in addition orm dataclasses and removed requires triage New issue that requires categorization labels May 29, 2023
@BranislavBajuzik
Copy link
Author

BranislavBajuzik commented May 29, 2023

I apologize for making the example unrunnable while trying to make a minimal example. I do not have much experience with SQLAlchemy, I am actually using it for the first time (I got excited about the typing 😅 )

I did subclass the DeclarativeBase and I still get this error:
image

As you can see, the database is created successfully, and the data is inserted, but Mypy is not happy. If I remove the MappedAsDataclass (and the init=False, pardon the screenshot), the error is gone:
image

Thank you

the code:

from sqlalchemy import create_engine
from sqlalchemy.orm import MappedAsDataclass, DeclarativeBase, Mapped, mapped_column, Session


class Base(DeclarativeBase):
    pass


class Test(MappedAsDataclass, Base):
    __tablename__ = "test"
    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    test_str_attribute: Mapped[str | None]


engine = create_engine("sqlite:///db.sqlite", echo=True)

Base.metadata.create_all(engine)

with Session(engine) as s:
    t = Test(test_str_attribute=None)
    s.add(t)
    s.commit()

@CaselIT
Copy link
Member

CaselIT commented May 29, 2023

This is a bug of MyPy. pyright or pylance type check the code correctly. cc @zzzeek
Example:

from typing import Any, Generic, TypeVar, dataclass_transform, reveal_type

T = TypeVar("T")

class Desc(Generic[T]):
    def __set__(self, instance: Any, value: "Desc[T]| T") -> None:
        ...

@dataclass_transform(field_specifiers=(Desc,))
class DescMeta(type):
    pass

class DescBase(metaclass=DescMeta):
    pass

class Foo(DescBase):
    bar: Desc[int | None]

reveal_type(Foo.__init__)
Foo(bar=None)

This prints in mypy

test1.py:19: note: Revealed type is "def (self: test1.Foo, bar: Union[test1.Desc[Union[builtins.int, None]], builtins.int])"
test1.py:20: error: Argument "bar" to "Foo" has incompatible type "None"; expected "Union[Desc[Optional[int]], int]"
  [arg-type]
Found 1 error in 1 file (checked 1 source file)

while pyright correctly says

test1.py:19:13 - information: Type of "Foo.__init__" is "(self: Foo, bar: Desc[int | None] | int | None) -> None"

@CaselIT CaselIT added external extension issues an extension to SQLAlchemy may be producing a problem and removed expected behavior that's how it's meant to work. consider the "documentation" label in addition labels May 29, 2023
@CaselIT
Copy link
Member

CaselIT commented May 29, 2023

@zzzeek I don't remember if you already reported this one to mypy

@zzzeek
Copy link
Member

zzzeek commented May 29, 2023

yes it's python/mypy#15020

@zzzeek
Copy link
Member

zzzeek commented May 29, 2023

so if that's the issue, then dupe of #9617

@zzzeek zzzeek added the duplicate This issue or pull request already exists label May 29, 2023
@CaselIT CaselIT added third party integration issues issues to do with other libraries and frameworks and removed external extension issues an extension to SQLAlchemy may be producing a problem labels Jun 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dataclasses duplicate This issue or pull request already exists orm third party integration issues issues to do with other libraries and frameworks typing pep -484 typing issues. independent of "mypy"
Projects
None yet
Development

No branches or pull requests

3 participants