summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql
diff options
context:
space:
mode:
authorFrederik Aalund <fpa@sbtinstruments.com>2023-01-30 11:50:40 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2023-01-31 22:02:30 -0500
commit8e890609eb47f5a273e695154cf143af56807921 (patch)
tree2b687da911107a5c4e86231a1241c5a6ee4dae3f /lib/sqlalchemy/sql
parenta21c715b7a89b0619db0d2d5b31617d17b25a27a (diff)
downloadsqlalchemy-8e890609eb47f5a273e695154cf143af56807921.tar.gz
Add support for typing.Literal in Mapped
Added support for :pep:`586` ``Literal`` to be used in the :paramref:`_orm.registry.type_annotation_map` as well as within :class:`.Mapped` constructs. To use custom types such as these, they must appear explicitly within the :paramref:`_orm.registry.type_annotation_map` to be mapped. Pull request courtesy Frederik Aalund. As part of this change, the support for :class:`.sqltypes.Enum` in the :paramref:`_orm.registry.type_annotation_map` has been expanded to include support for ``Literal[]`` types consisting of string values to be used, in addition to ``enum.Enum`` datatypes. If a ``Literal[]`` datatype is used within ``Mapped[]`` that is not linked in :paramref:`_orm.registry.type_annotation_map` to a specific datatype, a :class:`.sqltypes.Enum` will be used by default. Fixed issue involving the use of :class:`.sqltypes.Enum` within the :paramref:`_orm.registry.type_annotation_map` where the :paramref:`_sqltypes.Enum.native_enum` parameter would not be correctly copied to the mapped column datatype, if it were overridden as stated in the documentation to set this parameter to False. Fixes: #9187 Fixes: #9200 Closes: #9191 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/9191 Pull-request-sha: 7d13f705307bf62560fc831f6f049a425d411374 Change-Id: Ife3ba2655f4897f806d6a9cf0041c69fd4f39e9d
Diffstat (limited to 'lib/sqlalchemy/sql')
-rw-r--r--lib/sqlalchemy/sql/sqltypes.py72
1 files changed, 61 insertions, 11 deletions
diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py
index 717e6c0b2..b2dcc9b8a 100644
--- a/lib/sqlalchemy/sql/sqltypes.py
+++ b/lib/sqlalchemy/sql/sqltypes.py
@@ -59,7 +59,9 @@ from .. import util
from ..engine import processors
from ..util import langhelpers
from ..util import OrderedDict
+from ..util.typing import is_literal
from ..util.typing import Literal
+from ..util.typing import typing_get_args
if TYPE_CHECKING:
from ._typing import _ColumnExpressionArgument
@@ -1263,6 +1265,11 @@ class Enum(String, SchemaType, Emulated, TypeEngine[Union[str, enum.Enum]]):
.. seealso::
+ :ref:`orm_declarative_mapped_column_enums` - background on using
+ the :class:`_sqltypes.Enum` datatype with the ORM's
+ :ref:`ORM Annotated Declarative <orm_declarative_mapped_column>`
+ feature.
+
:class:`_postgresql.ENUM` - PostgreSQL-specific type,
which has additional functionality.
@@ -1504,16 +1511,54 @@ class Enum(String, SchemaType, Emulated, TypeEngine[Union[str, enum.Enum]]):
matched_on: _MatchedOnType,
matched_on_flattened: Type[Any],
) -> Optional[Enum]:
- if not issubclass(python_type, enum.Enum):
- return None
+
+ # "generic form" indicates we were placed in a type map
+ # as ``sqlalchemy.Enum(enum.Enum)`` which indicates we need to
+ # get enumerated values from the datatype
+ we_are_generic_form = self._enums_argument == [enum.Enum]
+
+ native_enum = None
+
+ if not we_are_generic_form and python_type is matched_on:
+ # if we have enumerated values, and the incoming python
+ # type is exactly the one that matched in the type map,
+ # then we use these enumerated values and dont try to parse
+ # what's incoming
+ enum_args = self._enums_argument
+
+ elif is_literal(python_type):
+ # for a literal, where we need to get its contents, parse it out.
+ enum_args = typing_get_args(python_type)
+ bad_args = [arg for arg in enum_args if not isinstance(arg, str)]
+ if bad_args:
+ raise exc.ArgumentError(
+ f"Can't create string-based Enum datatype from non-string "
+ f"values: {', '.join(repr(x) for x in bad_args)}. Please "
+ f"provide an explicit Enum datatype for this Python type"
+ )
+ native_enum = False
+ elif isinstance(python_type, type) and issubclass(
+ python_type, enum.Enum
+ ):
+ # same for an enum.Enum
+ enum_args = [python_type]
+
+ else:
+ enum_args = self._enums_argument
+
+ # make a new Enum that looks like this one.
+ # pop the "name" so that it gets generated based on the enum
+ # arguments or other rules
+ kw = self._make_enum_kw({})
+
+ kw.pop("name", None)
+ if native_enum is False:
+ kw["native_enum"] = False
+
+ kw["length"] = NO_ARG if self.length == 0 else self.length
return cast(
Enum,
- util.constructor_copy(
- self,
- self._generic_type_affinity,
- python_type,
- length=NO_ARG if self.length == 0 else self.length,
- ),
+ self._generic_type_affinity(_enums=enum_args, **kw), # type: ignore # noqa: E501
)
def _setup_for_values(self, values, objects, kw):
@@ -1622,19 +1667,23 @@ class Enum(String, SchemaType, Emulated, TypeEngine[Union[str, enum.Enum]]):
self, self._generic_type_affinity, *args, _disable_warnings=True
)
- def adapt_to_emulated(self, impltype, **kw):
+ def _make_enum_kw(self, kw):
kw.setdefault("validate_strings", self.validate_strings)
kw.setdefault("name", self.name)
- kw["_disable_warnings"] = True
kw.setdefault("schema", self.schema)
kw.setdefault("inherit_schema", self.inherit_schema)
kw.setdefault("metadata", self.metadata)
- kw.setdefault("_create_events", False)
kw.setdefault("native_enum", self.native_enum)
kw.setdefault("values_callable", self.values_callable)
kw.setdefault("create_constraint", self.create_constraint)
kw.setdefault("length", self.length)
kw.setdefault("omit_aliases", self._omit_aliases)
+ return kw
+
+ def adapt_to_emulated(self, impltype, **kw):
+ self._make_enum_kw(kw)
+ kw["_disable_warnings"] = True
+ kw.setdefault("_create_events", False)
assert "_enums" in kw
return impltype(**kw)
@@ -3702,6 +3751,7 @@ _type_map: Dict[Type[Any], TypeEngine[Any]] = {
bytes: LargeBinary(),
str: _STRING,
enum.Enum: Enum(enum.Enum),
+ Literal: Enum(enum.Enum), # type: ignore[dict-item]
}