diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-06-03 10:34:19 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-06-03 13:29:57 -0400 |
| commit | 47eff8b9e35dec9305d22484c17dd6c0649a876a (patch) | |
| tree | 83e806aab8759069b3d6847e063cb310f5bda74d /test | |
| parent | ad86d32f7fbd1c6deda8ff3bebe0595c0f2986cc (diff) | |
| download | sqlalchemy-47eff8b9e35dec9305d22484c17dd6c0649a876a.tar.gz | |
some typing fixes
* ClassVar for decl fields, add __tablename__
* dataclasses require annotations for all fields. For us,
if no annotation, then skip that field as part of what is
considered to be a "dataclass", as this matches the behavior
of pyright right now. We could alternatively raise on this
use, which is what dataclasses does. we should ask the pep
people
* plain field that's just "str", "int", etc., with no value.
Disallow it unless __allow_unmapped__ is set. If field
has dataclasses.field, Column, None, a value etc, it goes through,
and when using dataclasses mixin all such fields are considered
for the dataclass setup just like a dataclass. Hopefully this
does not have major backwards compat issues. __allow_unmapped__
can be set on the base class, mixins, etc., it's liberal for
now in case people have this problem.
* accommodate for ClassVar, these are not considered at all for
mapping.
Change-Id: Id743aa0456bade9a5d5832796caeecc3dc4accb7
Diffstat (limited to 'test')
| -rw-r--r-- | test/orm/declarative/test_dc_transforms.py | 123 | ||||
| -rw-r--r-- | test/orm/declarative/test_typed_mapping.py | 87 |
2 files changed, 201 insertions, 9 deletions
diff --git a/test/orm/declarative/test_dc_transforms.py b/test/orm/declarative/test_dc_transforms.py index 308ebfeb1..271b23596 100644 --- a/test/orm/declarative/test_dc_transforms.py +++ b/test/orm/declarative/test_dc_transforms.py @@ -2,6 +2,7 @@ import dataclasses import inspect as pyinspect from itertools import product from typing import Any +from typing import ClassVar from typing import List from typing import Optional from typing import Set @@ -59,8 +60,10 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase): __tablename__ = "b" id: Mapped[int] = mapped_column(primary_key=True, init=False) - a_id = mapped_column(ForeignKey("a.id"), init=False) data: Mapped[str] + a_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("a.id"), init=False + ) x: Mapped[Optional[int]] = mapped_column(default=None) A.__qualname__ = "some_module.A" @@ -102,6 +105,32 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase): a3 = A("data") eq_(repr(a3), "some_module.A(id=None, data='data', x=None, bs=[])") + def test_no_anno_doesnt_go_into_dc( + self, dc_decl_base: Type[MappedAsDataclass] + ): + class User(dc_decl_base): + __tablename__: ClassVar[Optional[str]] = "user" + + id: Mapped[int] = mapped_column(primary_key=True, init=False) + username: Mapped[str] + password: Mapped[str] + addresses: Mapped[List["Address"]] = relationship( # noqa: F821 + default_factory=list + ) + + class Address(dc_decl_base): + __tablename__: ClassVar[Optional[str]] = "address" + + id: Mapped[int] = mapped_column(primary_key=True, init=False) + + # should not be in the dataclass constructor + user_id = mapped_column(ForeignKey(User.id)) + + email_address: Mapped[str] + + a1 = Address("email@address") + eq_(a1.email_address, "email@address") + def test_basic_constructor_repr_cls_decorator( self, registry: _RegistryType ): @@ -156,11 +185,13 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase): ) a2 = A("10", x=5, bs=[B("data1"), B("data2", x=12)]) + + # note a_id isn't included because it wasn't annotated eq_( repr(a2), "some_module.A(id=None, data='10', x=5, " - "bs=[some_module.B(id=None, data='data1', a_id=None, x=None), " - "some_module.B(id=None, data='data2', a_id=None, x=12)])", + "bs=[some_module.B(id=None, data='data1', x=None), " + "some_module.B(id=None, data='data2', x=12)])", ) a3 = A("data") @@ -224,6 +255,50 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase): eq_(e1.engineer_name, "en") eq_(e1.primary_language, "pl") + def test_no_fields_wo_mapped_or_dc( + self, dc_decl_base: Type[MappedAsDataclass] + ): + """since I made this mistake in my own mapping video, lets have it + raise an error""" + + with expect_raises_message( + exc.ArgumentError, + r'Type annotation for "A.data" should ' + r'use the syntax "Mapped\[str\]". ' + r"To leave the attribute unmapped,", + ): + + class A(dc_decl_base): + __tablename__ = "a" + + id: Mapped[int] = mapped_column(primary_key=True, init=False) + data: str + ctrl_one: str = dataclasses.field() + some_field: int = dataclasses.field(default=5) + + def test_allow_unmapped_fields_wo_mapped_or_dc( + self, dc_decl_base: Type[MappedAsDataclass] + ): + class A(dc_decl_base): + __tablename__ = "a" + __allow_unmapped__ = True + + id: Mapped[int] = mapped_column(primary_key=True, init=False) + data: str + ctrl_one: str = dataclasses.field() + some_field: int = dataclasses.field(default=5) + + a1 = A("data", "ctrl_one", 5) + eq_( + dataclasses.asdict(a1), + { + "ctrl_one": "ctrl_one", + "data": "data", + "id": None, + "some_field": 5, + }, + ) + def test_integrated_dc(self, dc_decl_base: Type[MappedAsDataclass]): """We will be telling users "this is a dataclass that is also mapped". Therefore, they will want *any* kind of attribute to do what @@ -237,17 +312,48 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase): class A(dc_decl_base): __tablename__ = "a" - ctrl_one: str + ctrl_one: str = dataclasses.field() id: Mapped[int] = mapped_column(primary_key=True, init=False) data: Mapped[str] some_field: int = dataclasses.field(default=5) - some_none_field: Optional[str] = None + some_none_field: Optional[str] = dataclasses.field(default=None) + + some_other_int_field: int = 10 + # some field is part of the constructor a1 = A("ctrlone", "datafield") - eq_(a1.some_field, 5) - eq_(a1.some_none_field, None) + eq_( + dataclasses.asdict(a1), + { + "ctrl_one": "ctrlone", + "data": "datafield", + "id": None, + "some_field": 5, + "some_none_field": None, + "some_other_int_field": 10, + }, + ) + + a2 = A( + "ctrlone", + "datafield", + some_field=7, + some_other_int_field=12, + some_none_field="x", + ) + eq_( + dataclasses.asdict(a2), + { + "ctrl_one": "ctrlone", + "data": "datafield", + "id": None, + "some_field": 7, + "some_none_field": "x", + "some_other_int_field": 12, + }, + ) # only Mapped[] is mapped self.assert_compile(select(A), "SELECT a.id, a.data FROM a") @@ -260,10 +366,11 @@ class DCTransformsTest(AssertsCompiledSQL, fixtures.TestBase): "data", "some_field", "some_none_field", + "some_other_int_field", ], varargs=None, varkw=None, - defaults=(5, None), + defaults=(5, None, 10), kwonlyargs=[], kwonlydefaults=None, annotations={}, diff --git a/test/orm/declarative/test_typed_mapping.py b/test/orm/declarative/test_typed_mapping.py index ce8cd6bdf..01849a8ee 100644 --- a/test/orm/declarative/test_typed_mapping.py +++ b/test/orm/declarative/test_typed_mapping.py @@ -1,6 +1,7 @@ import dataclasses import datetime from decimal import Decimal +from typing import ClassVar from typing import Dict from typing import Generic from typing import List @@ -69,7 +70,9 @@ class DeclarativeBaseTest(fixtures.TestBase): class Tab(Base["Tab"]): __tablename__ = "foo" - a = Column(Integer, primary_key=True) + + # old mypy plugin use + a: int = Column(Integer, primary_key=True) eq_(Tab.foo, 1) is_(Tab.__table__, inspect(Tab).local_table) @@ -192,6 +195,88 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL): is_true(User.__table__.c.data.nullable) assert isinstance(User.__table__.c.created_at.type, DateTime) + def test_i_have_a_classvar_on_my_class(self, decl_base): + class MyClass(decl_base): + __tablename__ = "mytable" + + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] = mapped_column(default="some default") + + status: ClassVar[int] + + m1 = MyClass(id=1, data=5) + assert "status" not in inspect(m1).mapper.attrs + + def test_i_have_plain_or_column_attrs_on_my_class_w_values( + self, decl_base + ): + class MyClass(decl_base): + __tablename__ = "mytable" + + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] = mapped_column(default="some default") + + old_column: str = Column(String) + + # we assume this is intentional + status: int = 5 + + # it's mapped too + assert "old_column" in inspect(MyClass).attrs + + def test_i_have_plain_attrs_on_my_class_disallowed(self, decl_base): + with expect_raises_message( + sa_exc.ArgumentError, + r'Type annotation for "MyClass.status" should use the syntax ' + r'"Mapped\[int\]". To leave the attribute unmapped, use ' + r"ClassVar\[int\], assign a value to the attribute, or " + r"set __allow_unmapped__ = True on the class.", + ): + + class MyClass(decl_base): + __tablename__ = "mytable" + + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] = mapped_column(default="some default") + + # we assume this is not intentional. because I made the + # same mistake myself :) + status: int + + def test_i_have_plain_attrs_on_my_class_allowed(self, decl_base): + class MyClass(decl_base): + __tablename__ = "mytable" + __allow_unmapped__ = True + + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] = mapped_column(default="some default") + + status: int + + def test_allow_unmapped_on_mixin(self, decl_base): + class AllowsUnmapped: + __allow_unmapped__ = True + + class MyClass(AllowsUnmapped, decl_base): + __tablename__ = "mytable" + + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] = mapped_column(default="some default") + + status: int + + def test_allow_unmapped_on_base(self): + class Base(DeclarativeBase): + __allow_unmapped__ = True + + class MyClass(Base): + __tablename__ = "mytable" + + id: Mapped[int] = mapped_column(primary_key=True) + data: Mapped[str] = mapped_column(default="some default") + + status: int + def test_column_default(self, decl_base): class MyClass(decl_base): __tablename__ = "mytable" |
