--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 12207
+
+ Fixed issues in type handling within the ``type_annotation_map`` feature
+ which prevented the use of unions, using either pep-604 or ``Union``
+ syntaxes under future annotations mode, which contained multiple generic
+ types as elements from being correctly resolvable.
type(attr_value),
required=False,
is_dataclass_field=is_dataclass_field,
- expect_mapped=expect_mapped
- and not is_dataclass, # self.allow_dataclass_fields,
+ expect_mapped=expect_mapped and not is_dataclass,
)
-
if extracted is None:
# ClassVar can come out here
return None
if attr_value is None and not is_literal(extracted_mapped_annotation):
for elem in get_args(extracted_mapped_annotation):
- if isinstance(elem, str) or is_fwd_ref(
- elem, check_generic=True
+ if is_fwd_ref(
+ elem, check_generic=True, check_for_plain_string=True
):
elem = de_stringify_annotation(
self.cls,
from .interfaces import StrategizedProperty
from .relationships import RelationshipProperty
from .util import de_stringify_annotation
-from .util import de_stringify_union_elements
from .. import exc as sa_exc
from .. import ForeignKey
from .. import log
from ..util.typing import is_fwd_ref
from ..util.typing import is_pep593
from ..util.typing import is_pep695
-from ..util.typing import is_union
from ..util.typing import Self
if TYPE_CHECKING:
) -> None:
sqltype = self.column.type
- if isinstance(argument, str) or is_fwd_ref(
- argument, check_generic=True
+ if is_fwd_ref(
+ argument, check_generic=True, check_for_plain_string=True
):
assert originating_module is not None
argument = de_stringify_annotation(
cls, argument, originating_module, include_generic=True
)
- if is_union(argument):
- assert originating_module is not None
- argument = de_stringify_union_elements(
- cls, argument, originating_module
- )
-
nullable = includes_none(argument)
if not self._has_nullable:
from ..sql.selectable import FromClause
from ..util.langhelpers import MemoizedSlots
from ..util.typing import de_stringify_annotation as _de_stringify_annotation
-from ..util.typing import (
- de_stringify_union_elements as _de_stringify_union_elements,
-)
from ..util.typing import eval_name_only as _eval_name_only
from ..util.typing import fixup_container_fwd_refs
from ..util.typing import get_origin
from ..sql.selectable import Selectable
from ..sql.visitors import anon_map
from ..util.typing import _AnnotationScanType
- from ..util.typing import ArgsTypeProtocol
_T = TypeVar("_T", bound=Any)
)
)
-
_de_stringify_partial = functools.partial(
functools.partial,
locals_=util.immutabledict(
)
-class _DeStringifyUnionElements(Protocol):
- def __call__(
- self,
- cls: Type[Any],
- annotation: ArgsTypeProtocol,
- originating_module: str,
- *,
- str_cleanup_fn: Optional[Callable[[str, str], str]] = None,
- ) -> Type[Any]: ...
-
-
-de_stringify_union_elements = cast(
- _DeStringifyUnionElements,
- _de_stringify_partial(_de_stringify_union_elements),
-)
-
-
class _EvalNameOnly(Protocol):
def __call__(self, name: str, module_name: str) -> Any: ...
inner: Optional[Match[str]]
- mm = re.match(r"^(.+?)\[(.+)\]$", annotation)
+ mm = re.match(r"^([^ \|]+?)\[(.+)\]$", annotation)
if not mm:
return annotation
while True:
stack.append(real_symbol if mm is inner else inner.group(1))
g2 = inner.group(2)
- inner = re.match(r"^(.+?)\[(.+)\]$", g2)
+ inner = re.match(r"^([^ \|]+?)\[(.+)\]$", g2)
if inner is None:
stack.append(g2)
break
# ['Mapped', "'Optional[Dict[str, str]]'"]
not re.match(r"""^["'].*["']$""", stack[-1])
# avoid further generics like Dict[] such as
- # ['Mapped', 'dict[str, str] | None']
- and not re.match(r".*\[.*\]", stack[-1])
+ # ['Mapped', 'dict[str, str] | None'],
+ # ['Mapped', 'list[int] | list[str]'],
+ # ['Mapped', 'Union[list[int], list[str]]'],
+ and not re.search(r"[\[\]]", stack[-1])
):
stripchars = "\"' "
stack[-1] = ", ".join(
return None
try:
+ # destringify the "outside" of the annotation. note we are not
+ # adding include_generic so it will *not* dig into generic contents,
+ # which will remain as ForwardRef or plain str under future annotations
+ # mode. The full destringify happens later when mapped_column goes
+ # to do a full lookup in the registry type_annotations_map.
annotated = de_stringify_annotation(
cls,
raw_annotation,
return getattr(obj, "__name__", name)
-def de_stringify_union_elements(
- cls: Type[Any],
- annotation: ArgsTypeProtocol,
- originating_module: str,
- locals_: Mapping[str, Any],
- *,
- str_cleanup_fn: Optional[Callable[[str, str], str]] = None,
-) -> Type[Any]:
- return make_union_type(
- *[
- de_stringify_annotation(
- cls,
- anno,
- originating_module,
- {},
- str_cleanup_fn=str_cleanup_fn,
- )
- for anno in annotation.__args__
- ]
- )
-
-
def is_pep593(type_: Optional[Any]) -> bool:
return type_ is not None and get_origin(type_) is Annotated
def is_fwd_ref(
- type_: _AnnotationScanType, check_generic: bool = False
+ type_: _AnnotationScanType,
+ check_generic: bool = False,
+ check_for_plain_string: bool = False,
) -> TypeGuard[ForwardRef]:
- if isinstance(type_, ForwardRef):
+ if check_for_plain_string and isinstance(type_, str):
+ return True
+ elif isinstance(type_, ForwardRef):
return True
elif check_generic and is_generic(type_):
- return any(is_fwd_ref(arg, True) for arg in type_.__args__)
+ return any(
+ is_fwd_ref(
+ arg, True, check_for_plain_string=check_for_plain_string
+ )
+ for arg in type_.__args__
+ )
else:
return False
"""Given a type, filter out ``Union`` types that include ``NoneType``
to not include the ``NoneType``.
+ Contains extra logic to work on non-flattened unions, unions that contain
+ ``None`` (seen in py38, 37)
+
"""
if is_fwd_ref(type_):
return _de_optionalize_fwd_ref_union_types(type_, False)
elif is_union(type_) and includes_none(type_):
- typ = set(type_.__args__)
+ if compat.py39:
+ typ = set(type_.__args__)
+ else:
+ # py38, 37 - unions are not automatically flattened, can contain
+ # None rather than NoneType
+ stack_of_unions = deque([type_])
+ typ = set()
+ while stack_of_unions:
+ u_typ = stack_of_unions.popleft()
+ for elem in u_typ.__args__:
+ if is_union(elem):
+ stack_of_unions.append(elem)
+ else:
+ typ.add(elem)
+
+ typ.discard(None) # type: ignore
typ.discard(NoneType)
typ.discard(NoneFwd)
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
+from sqlalchemy.orm.util import _cleanup_mapped_str_annotation
from sqlalchemy.sql import sqltypes
from sqlalchemy.testing import eq_
from sqlalchemy.testing import expect_raises_message
+from sqlalchemy.testing import fixtures
from sqlalchemy.testing import is_
from sqlalchemy.testing import is_true
from .test_typed_mapping import expect_annotation_syntax_error
pass
+class AnnoUtilTest(fixtures.TestBase):
+ @testing.combinations(
+ ("Mapped[Address]", 'Mapped["Address"]'),
+ ('Mapped["Address"]', 'Mapped["Address"]'),
+ ("Mapped['Address']", "Mapped['Address']"),
+ ("Mapped[Address | None]", 'Mapped["Address | None"]'),
+ ("Mapped[None | Address]", 'Mapped["None | Address"]'),
+ ('Mapped["Address | None"]', 'Mapped["Address | None"]'),
+ ("Mapped['None | Address']", "Mapped['None | Address']"),
+ ('Mapped["Address" | "None"]', 'Mapped["Address" | "None"]'),
+ ('Mapped["None" | "Address"]', 'Mapped["None" | "Address"]'),
+ ("Mapped[A_]", 'Mapped["A_"]'),
+ ("Mapped[_TypingLiteral]", 'Mapped["_TypingLiteral"]'),
+ ("Mapped[datetime.datetime]", 'Mapped["datetime.datetime"]'),
+ ("Mapped[List[Edge]]", 'Mapped[List["Edge"]]'),
+ (
+ "Mapped[collections.abc.MutableSequence[B]]",
+ 'Mapped[collections.abc.MutableSequence["B"]]',
+ ),
+ ("Mapped[typing.Sequence[B]]", 'Mapped[typing.Sequence["B"]]'),
+ ("Mapped[dict[str, str]]", 'Mapped[dict["str", "str"]]'),
+ ("Mapped[Dict[str, str]]", 'Mapped[Dict["str", "str"]]'),
+ ("Mapped[list[str]]", 'Mapped[list["str"]]'),
+ ("Mapped[dict[str, str] | None]", "Mapped[dict[str, str] | None]"),
+ ("Mapped[Optional[anno_str_mc]]", 'Mapped[Optional["anno_str_mc"]]'),
+ (
+ "Mapped[Optional[Dict[str, str]]]",
+ 'Mapped[Optional[Dict["str", "str"]]]',
+ ),
+ (
+ "Mapped[Optional[Union[Decimal, float]]]",
+ 'Mapped[Optional[Union["Decimal", "float"]]]',
+ ),
+ (
+ "Mapped[Optional[Union[list[int], list[str]]]]",
+ "Mapped[Optional[Union[list[int], list[str]]]]",
+ ),
+ ("Mapped[TestType[str]]", 'Mapped[TestType["str"]]'),
+ ("Mapped[TestType[str, str]]", 'Mapped[TestType["str", "str"]]'),
+ ("Mapped[Union[A, None]]", 'Mapped[Union["A", "None"]]'),
+ ("Mapped[Union[Decimal, float]]", 'Mapped[Union["Decimal", "float"]]'),
+ (
+ "Mapped[Union[Decimal, float, None]]",
+ 'Mapped[Union["Decimal", "float", "None"]]',
+ ),
+ (
+ "Mapped[Union[Dict[str, str], None]]",
+ "Mapped[Union[Dict[str, str], None]]",
+ ),
+ ("Mapped[Union[float, Decimal]]", 'Mapped[Union["float", "Decimal"]]'),
+ (
+ "Mapped[Union[list[int], list[str]]]",
+ "Mapped[Union[list[int], list[str]]]",
+ ),
+ (
+ "Mapped[Union[list[int], list[str], None]]",
+ "Mapped[Union[list[int], list[str], None]]",
+ ),
+ (
+ "Mapped[Union[None, Dict[str, str]]]",
+ "Mapped[Union[None, Dict[str, str]]]",
+ ),
+ (
+ "Mapped[Union[None, list[int], list[str]]]",
+ "Mapped[Union[None, list[int], list[str]]]",
+ ),
+ ("Mapped[A | None]", 'Mapped["A | None"]'),
+ ("Mapped[Decimal | float]", 'Mapped["Decimal | float"]'),
+ ("Mapped[Decimal | float | None]", 'Mapped["Decimal | float | None"]'),
+ (
+ "Mapped[list[int] | list[str] | None]",
+ "Mapped[list[int] | list[str] | None]",
+ ),
+ ("Mapped[None | dict[str, str]]", "Mapped[None | dict[str, str]]"),
+ (
+ "Mapped[None | list[int] | list[str]]",
+ "Mapped[None | list[int] | list[str]]",
+ ),
+ )
+ def test_cleanup_mapped_str_annotation(self, given, expected):
+ eq_(_cleanup_mapped_str_annotation(given, __name__), expected)
+
+
class MappedColumnTest(_MappedColumnTest):
def test_fully_qualified_mapped_name(self, decl_base):
"""test #8853, regression caused by #8759 ;)
_StrTypeAlias: TypeAlias = str
-_StrPep695: TypeAlias = str
-_UnionPep695: TypeAlias = Union[_SomeDict1, _SomeDict2]
+if TYPE_CHECKING:
+ _StrPep695: TypeAlias = str
+ _UnionPep695: TypeAlias = Union[_SomeDict1, _SomeDict2]
if compat.py38:
_TypingLiteral = typing.Literal["a", "b"]
)
+def make_pep695_type(name, definition):
+ lcls = {}
+ exec(
+ f"""
+type {name} = {definition}
+""",
+ lcls,
+ )
+ return lcls[name]
+
+
def expect_annotation_syntax_error(name):
return expect_raises_message(
sa_exc.ArgumentError,
"optional",
"optional_union",
"optional_union_604",
+ "union_newtype",
+ "union_null_newtype",
+ "union_695",
+ "union_null_695",
],
)
@testing.variation("in_map", ["yes", "no", "value"])
tat = TypeAliasType("tat", Optional[Union[str, int]])
elif option.optional_union_604:
tat = TypeAliasType("tat", Optional[str | int])
+ elif option.union_newtype:
+ # this seems to be illegal for typing but "works"
+ tat = NewType("tat", Union[str, int])
+ elif option.union_null_newtype:
+ # this seems to be illegal for typing but "works"
+ tat = NewType("tat", Union[str, int, None])
+ elif option.union_695:
+ tat = make_pep695_type("tat", str | int)
+ elif option.union_null_695:
+ tat = make_pep695_type("tat", str | int | None)
else:
option.fail()
if in_map.yes:
decl_base.registry.update_type_annotation_map({tat: String(99)})
- elif in_map.value:
+ elif in_map.value and "newtype" not in option.name:
decl_base.registry.update_type_annotation_map(
{tat.__value__: String(99)}
)
if in_map.yes:
col = declare()
length = 99
- elif in_map.value or option.optional or option.plain:
+ elif (
+ in_map.value
+ and "newtype" not in option.name
+ or option.optional
+ or option.plain
+ ):
with expect_deprecated(
"Matching the provided TypeAliasType 'tat' on its "
"resolved value without matching it in the "
refer_union: Mapped[UnionType]
refer_union_optional: Mapped[Optional[UnionType]]
+ # py38, 37 does not automatically flatten unions, add extra tests
+ # for this. maintain these in order to catch future regressions
+ # in the behavior of ``Union``
+ unflat_union_optional_data: Mapped[
+ Union[Union[Decimal, float, None], None]
+ ] = mapped_column()
+
float_data: Mapped[float] = mapped_column()
decimal_data: Mapped[Decimal] = mapped_column()
("reverse_u_optional_data", True),
("refer_union", "null" in union.name),
("refer_union_optional", True),
+ ("unflat_union_optional_data", True),
]
if compat.py310:
info += [
is_true(A.__table__.c.json1.nullable)
is_false(A.__table__.c.json2.nullable)
- @testing.combinations(
- ("not_optional",),
- ("optional",),
- ("optional_fwd_ref",),
- ("union_none",),
- ("pep604", testing.requires.python310),
- ("pep604_fwd_ref", testing.requires.python310),
- argnames="optional_on_json",
+ @testing.variation(
+ "option",
+ [
+ "not_optional",
+ "optional",
+ "optional_fwd_ref",
+ "union_none",
+ ("pep604", testing.requires.python310),
+ ("pep604_fwd_ref", testing.requires.python310),
+ ],
)
+ @testing.variation("brackets", ["oneset", "twosets"])
@testing.combinations(
"include_mc_type", "derive_from_anno", argnames="include_mc_type"
)
def test_optional_styles_nested_brackets(
- self, optional_on_json, include_mc_type
+ self, option, brackets, include_mc_type
):
+ """composed types test, includes tests that were added later for
+ #12207"""
+
class Base(DeclarativeBase):
if testing.requires.python310.enabled:
type_annotation_map = {
- Dict[str, str]: JSON,
- dict[str, str]: JSON,
+ Dict[str, Decimal]: JSON,
+ dict[str, Decimal]: JSON,
+ Union[List[int], List[str]]: JSON,
+ list[int] | list[str]: JSON,
}
else:
type_annotation_map = {
- Dict[str, str]: JSON,
+ Dict[str, Decimal]: JSON,
+ Union[List[int], List[str]]: JSON,
}
if include_mc_type == "include_mc_type":
mc = mapped_column(JSON)
+ mc2 = mapped_column(JSON)
else:
mc = mapped_column()
+ mc2 = mapped_column()
class A(Base):
__tablename__ = "a"
id: Mapped[int] = mapped_column(primary_key=True)
data: Mapped[str] = mapped_column()
- if optional_on_json == "not_optional":
- json: Mapped[Dict[str, str]] = mapped_column() # type: ignore
- elif optional_on_json == "optional":
- json: Mapped[Optional[Dict[str, str]]] = mc
- elif optional_on_json == "optional_fwd_ref":
- json: Mapped["Optional[Dict[str, str]]"] = mc
- elif optional_on_json == "union_none":
- json: Mapped[Union[Dict[str, str], None]] = mc
- elif optional_on_json == "pep604":
- json: Mapped[dict[str, str] | None] = mc
- elif optional_on_json == "pep604_fwd_ref":
- json: Mapped["dict[str, str] | None"] = mc
+ if brackets.oneset:
+ if option.not_optional:
+ json: Mapped[Dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
+ if testing.requires.python310.enabled:
+ json2: Mapped[dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
+ elif option.optional:
+ json: Mapped[Optional[Dict[str, Decimal]]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[Optional[dict[str, Decimal]]] = mc2
+ elif option.optional_fwd_ref:
+ json: Mapped["Optional[Dict[str, Decimal]]"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped["Optional[dict[str, Decimal]]"] = mc2
+ elif option.union_none:
+ json: Mapped[Union[Dict[str, Decimal], None]] = mc
+ json2: Mapped[Union[None, Dict[str, Decimal]]] = mc2
+ elif option.pep604:
+ json: Mapped[dict[str, Decimal] | None] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[None | dict[str, Decimal]] = mc2
+ elif option.pep604_fwd_ref:
+ json: Mapped["dict[str, Decimal] | None"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped["None | dict[str, Decimal]"] = mc2
+ elif brackets.twosets:
+ if option.not_optional:
+ json: Mapped[Union[List[int], List[str]]] = mapped_column() # type: ignore # noqa: E501
+ elif option.optional:
+ json: Mapped[Optional[Union[List[int], List[str]]]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[
+ Optional[Union[list[int], list[str]]]
+ ] = mc2
+ elif option.optional_fwd_ref:
+ json: Mapped["Optional[Union[List[int], List[str]]]"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[
+ "Optional[Union[list[int], list[str]]]"
+ ] = mc2
+ elif option.union_none:
+ json: Mapped[Union[List[int], List[str], None]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[Union[None, list[int], list[str]]] = mc2
+ elif option.pep604:
+ json: Mapped[list[int] | list[str] | None] = mc
+ json2: Mapped[None | list[int] | list[str]] = mc2
+ elif option.pep604_fwd_ref:
+ json: Mapped["list[int] | list[str] | None"] = mc
+ json2: Mapped["None | list[int] | list[str]"] = mc2
+ else:
+ brackets.fail()
is_(A.__table__.c.json.type._type_affinity, JSON)
- if optional_on_json == "not_optional":
+ if hasattr(A, "json2"):
+ is_(A.__table__.c.json2.type._type_affinity, JSON)
+ if option.not_optional:
+ is_false(A.__table__.c.json2.nullable)
+ else:
+ is_true(A.__table__.c.json2.nullable)
+
+ if option.not_optional:
is_false(A.__table__.c.json.nullable)
else:
is_true(A.__table__.c.json.nullable)
back_populates="bs", primaryjoin=a_id == A.id
)
elif optional_on_m2o == "union_none":
- a: Mapped["Union[A, None]"] = relationship(
+ a: Mapped[Union[A, None]] = relationship(
back_populates="bs", primaryjoin=a_id == A.id
)
elif optional_on_m2o == "pep604":
_StrTypeAlias: TypeAlias = str
-_StrPep695: TypeAlias = str
-_UnionPep695: TypeAlias = Union[_SomeDict1, _SomeDict2]
+if TYPE_CHECKING:
+ _StrPep695: TypeAlias = str
+ _UnionPep695: TypeAlias = Union[_SomeDict1, _SomeDict2]
if compat.py38:
_TypingLiteral = typing.Literal["a", "b"]
)
+def make_pep695_type(name, definition):
+ lcls = {}
+ exec(
+ f"""
+type {name} = {definition}
+""",
+ lcls,
+ )
+ return lcls[name]
+
+
def expect_annotation_syntax_error(name):
return expect_raises_message(
sa_exc.ArgumentError,
"optional",
"optional_union",
"optional_union_604",
+ "union_newtype",
+ "union_null_newtype",
+ "union_695",
+ "union_null_695",
],
)
@testing.variation("in_map", ["yes", "no", "value"])
tat = TypeAliasType("tat", Optional[Union[str, int]])
elif option.optional_union_604:
tat = TypeAliasType("tat", Optional[str | int])
+ elif option.union_newtype:
+ # this seems to be illegal for typing but "works"
+ tat = NewType("tat", Union[str, int])
+ elif option.union_null_newtype:
+ # this seems to be illegal for typing but "works"
+ tat = NewType("tat", Union[str, int, None])
+ elif option.union_695:
+ tat = make_pep695_type("tat", str | int)
+ elif option.union_null_695:
+ tat = make_pep695_type("tat", str | int | None)
else:
option.fail()
if in_map.yes:
decl_base.registry.update_type_annotation_map({tat: String(99)})
- elif in_map.value:
+ elif in_map.value and "newtype" not in option.name:
decl_base.registry.update_type_annotation_map(
{tat.__value__: String(99)}
)
if in_map.yes:
col = declare()
length = 99
- elif in_map.value or option.optional or option.plain:
+ elif (
+ in_map.value
+ and "newtype" not in option.name
+ or option.optional
+ or option.plain
+ ):
with expect_deprecated(
"Matching the provided TypeAliasType 'tat' on its "
"resolved value without matching it in the "
refer_union: Mapped[UnionType]
refer_union_optional: Mapped[Optional[UnionType]]
+ # py38, 37 does not automatically flatten unions, add extra tests
+ # for this. maintain these in order to catch future regressions
+ # in the behavior of ``Union``
+ unflat_union_optional_data: Mapped[
+ Union[Union[Decimal, float, None], None]
+ ] = mapped_column()
+
float_data: Mapped[float] = mapped_column()
decimal_data: Mapped[Decimal] = mapped_column()
("reverse_u_optional_data", True),
("refer_union", "null" in union.name),
("refer_union_optional", True),
+ ("unflat_union_optional_data", True),
]
if compat.py310:
info += [
is_true(A.__table__.c.json1.nullable)
is_false(A.__table__.c.json2.nullable)
- @testing.combinations(
- ("not_optional",),
- ("optional",),
- ("optional_fwd_ref",),
- ("union_none",),
- ("pep604", testing.requires.python310),
- ("pep604_fwd_ref", testing.requires.python310),
- argnames="optional_on_json",
+ @testing.variation(
+ "option",
+ [
+ "not_optional",
+ "optional",
+ "optional_fwd_ref",
+ "union_none",
+ ("pep604", testing.requires.python310),
+ ("pep604_fwd_ref", testing.requires.python310),
+ ],
)
+ @testing.variation("brackets", ["oneset", "twosets"])
@testing.combinations(
"include_mc_type", "derive_from_anno", argnames="include_mc_type"
)
def test_optional_styles_nested_brackets(
- self, optional_on_json, include_mc_type
+ self, option, brackets, include_mc_type
):
+ """composed types test, includes tests that were added later for
+ #12207"""
+
class Base(DeclarativeBase):
if testing.requires.python310.enabled:
type_annotation_map = {
- Dict[str, str]: JSON,
- dict[str, str]: JSON,
+ Dict[str, Decimal]: JSON,
+ dict[str, Decimal]: JSON,
+ Union[List[int], List[str]]: JSON,
+ list[int] | list[str]: JSON,
}
else:
type_annotation_map = {
- Dict[str, str]: JSON,
+ Dict[str, Decimal]: JSON,
+ Union[List[int], List[str]]: JSON,
}
if include_mc_type == "include_mc_type":
mc = mapped_column(JSON)
+ mc2 = mapped_column(JSON)
else:
mc = mapped_column()
+ mc2 = mapped_column()
class A(Base):
__tablename__ = "a"
id: Mapped[int] = mapped_column(primary_key=True)
data: Mapped[str] = mapped_column()
- if optional_on_json == "not_optional":
- json: Mapped[Dict[str, str]] = mapped_column() # type: ignore
- elif optional_on_json == "optional":
- json: Mapped[Optional[Dict[str, str]]] = mc
- elif optional_on_json == "optional_fwd_ref":
- json: Mapped["Optional[Dict[str, str]]"] = mc
- elif optional_on_json == "union_none":
- json: Mapped[Union[Dict[str, str], None]] = mc
- elif optional_on_json == "pep604":
- json: Mapped[dict[str, str] | None] = mc
- elif optional_on_json == "pep604_fwd_ref":
- json: Mapped["dict[str, str] | None"] = mc
+ if brackets.oneset:
+ if option.not_optional:
+ json: Mapped[Dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
+ if testing.requires.python310.enabled:
+ json2: Mapped[dict[str, Decimal]] = mapped_column() # type: ignore # noqa: E501
+ elif option.optional:
+ json: Mapped[Optional[Dict[str, Decimal]]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[Optional[dict[str, Decimal]]] = mc2
+ elif option.optional_fwd_ref:
+ json: Mapped["Optional[Dict[str, Decimal]]"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped["Optional[dict[str, Decimal]]"] = mc2
+ elif option.union_none:
+ json: Mapped[Union[Dict[str, Decimal], None]] = mc
+ json2: Mapped[Union[None, Dict[str, Decimal]]] = mc2
+ elif option.pep604:
+ json: Mapped[dict[str, Decimal] | None] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[None | dict[str, Decimal]] = mc2
+ elif option.pep604_fwd_ref:
+ json: Mapped["dict[str, Decimal] | None"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped["None | dict[str, Decimal]"] = mc2
+ elif brackets.twosets:
+ if option.not_optional:
+ json: Mapped[Union[List[int], List[str]]] = mapped_column() # type: ignore # noqa: E501
+ elif option.optional:
+ json: Mapped[Optional[Union[List[int], List[str]]]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[
+ Optional[Union[list[int], list[str]]]
+ ] = mc2
+ elif option.optional_fwd_ref:
+ json: Mapped["Optional[Union[List[int], List[str]]]"] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[
+ "Optional[Union[list[int], list[str]]]"
+ ] = mc2
+ elif option.union_none:
+ json: Mapped[Union[List[int], List[str], None]] = mc
+ if testing.requires.python310.enabled:
+ json2: Mapped[Union[None, list[int], list[str]]] = mc2
+ elif option.pep604:
+ json: Mapped[list[int] | list[str] | None] = mc
+ json2: Mapped[None | list[int] | list[str]] = mc2
+ elif option.pep604_fwd_ref:
+ json: Mapped["list[int] | list[str] | None"] = mc
+ json2: Mapped["None | list[int] | list[str]"] = mc2
+ else:
+ brackets.fail()
is_(A.__table__.c.json.type._type_affinity, JSON)
- if optional_on_json == "not_optional":
+ if hasattr(A, "json2"):
+ is_(A.__table__.c.json2.type._type_affinity, JSON)
+ if option.not_optional:
+ is_false(A.__table__.c.json2.nullable)
+ else:
+ is_true(A.__table__.c.json2.nullable)
+
+ if option.not_optional:
is_false(A.__table__.c.json.nullable)
else:
is_true(A.__table__.c.json.nullable)
back_populates="bs", primaryjoin=a_id == A.id
)
elif optional_on_m2o == "union_none":
- a: Mapped["Union[A, None]"] = relationship(
+ a: Mapped[Union[A, None]] = relationship(
back_populates="bs", primaryjoin=a_id == A.id
)
elif optional_on_m2o == "pep604":