Open mberdyshev opened 1 month ago
Hi,
Managing all attributes of enum with a type decorator is not easy, so it's likely missing something here.
If your use case I would more simply do something like
def Enum1(*args,**kwargs):
kwargs.set_default('validate_strings', True)
return Enum(*args, **kwargs)
In any case it's a valid bug
I've looked up the source code and found that type rendering depends on __repr__()
. So, inside SQLAlchemy I managed to find that Enum and TypeDecorator representations are different. To sum it up, the difference is the following:
enum = Enum(TestEnum)
print(util.generic_repr(enum)) # TypeDecorator
# Enum('FIRST', 'SECOND')
print(util.generic_repr(enum, to_inspect=[Enum, SchemaType])) # Enum
# Enum('FIRST', 'SECOND', name='testenum')
It seems now that this bug is more related to SQLAlchemy. Probably TypeDecorator's __repr__()
should respect the contained type's __repr__()
.
nice finding. Seems like that may be the issue. I'm not sure why there is a reason for that repr in type decorator (there usually is).
@zzzeek maybe a type should have a generic method to format itself that type decorator could hook into?
But the TypeDecorator does not repr() as the inner type does, it has its ownr repr() that does something different:
from sqlalchemy import *
print(repr(Enum("a", "b", "c", name="e1")))
class MyType(TypeDecorator):
impl = Enum
print(repr(MyType("a", "b", "c", name="e2")))
prints:
Enum('a', 'b', 'c', name='e1')
MyType('a', 'b', 'c')
note it uses MyType
. so that's not just calling Enum.__repr__()
. also no I'm not going to take the string from Enum.__repr__()
and manipulate it, that's too much guessing.
so there's no quick fix here. choices include:
__repr__()
into commands that are used to build out a composed reprthis would be the most expedient fix for the moment, but it wouldn't get the extra arguments used by enum beyond those used for schema type:
diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py
index bc2d898ab..6676a61e0 100644
--- a/lib/sqlalchemy/sql/sqltypes.py
+++ b/lib/sqlalchemy/sql/sqltypes.py
@@ -3826,3 +3826,4 @@ type_api.MATCHTYPE = MATCHTYPE
type_api.INDEXABLE = INDEXABLE = Indexable
type_api.TABLEVALUE = TABLEVALUE
type_api._resolve_value_to_type = _resolve_value_to_type
+type_api.SchemaType = SchemaType
diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py
index 9f40905fa..f484e26db 100644
--- a/lib/sqlalchemy/sql/type_api.py
+++ b/lib/sqlalchemy/sql/type_api.py
@@ -58,6 +58,7 @@ if typing.TYPE_CHECKING:
from .sqltypes import NUMERICTYPE as NUMERICTYPE # noqa: F401
from .sqltypes import STRINGTYPE as STRINGTYPE # noqa: F401
from .sqltypes import TABLEVALUE as TABLEVALUE # noqa: F401
+ from .sqltypes import SchemaType as SchemaType # noqa: F401
from ..engine.interfaces import Dialect
from ..util.typing import GenericProtocol
@@ -2276,7 +2277,13 @@ class TypeDecorator(SchemaEventTarget, ExternalType, TypeEngine[_T]):
return self.impl_instance.sort_key_function
def __repr__(self) -> str:
- return util.generic_repr(self, to_inspect=self.impl_instance)
+ inst = self.impl_instance
+ if isinstance(inst, SchemaType):
+ to_inspect=[inst, SchemaType]
+ else:
+ to_inspect = [inst]
+
+ return util.generic_repr(self, to_inspect=to_inspect)
class Variant(TypeDecorator[_T]):
But the TypeDecorator does not repr() as the inner type does, it has its ownr repr() that does something different:
I know, that's why I suggested to have new methods for this. regarding the fix you proposed it's likely better to have a proper method for it. the effort should be similar
For what it's worth, I ran into a similar issue. I have a PydanticModelType
generic
class PydanticModelType(TypeDecorator, Generic[T]):
"""Generic class representing a pydantic model that can be serialized to Python."""
cache_ok = True
impl = JSON()
def __init__(self, pydantic_type: type[T]) -> None:
"""Initializes class with the type of the pydantic model."""
self.pydantic_type = pydantic_type
super().__init__()
@override
def load_dialect_impl(self, dialect: Dialect) -> TypeEngine:
# Use JSONB for PostgreSQL and JSON for other databases.
if dialect.name == "postgresql":
return dialect.type_descriptor(JSONB())
return dialect.type_descriptor(JSON())
@override
def process_bind_param(self, value: T | None, _dialect: Dialect) -> str | None:
if value is not None:
value = self.pydantic_type.model_dump_json(value)
return value
@override
def process_result_value(
self, value: str | None, _dialect: Dialect
) -> BaseModel | None:
if value is not None:
value = self.pydantic_type.model_validate_json(value)
return value
And when I declare it in my class
configuration: Mapped[Config] = mapped_column(
PydanticModelType(Config), nullable=False
)
Alembic generates this:
sa.Column('configuration', app.database.types.PydanticModelType(), nullable=False),
Not sure if it's the same bug. I'll try overriding repr later.
Describe the bug A migration doesn't provide the
name
property of Enum ifsqlalchemy.Enum
is augmented withsqlalchemy.types.Decorator
. Even if thename
property is explicitly given.Expected behavior The property has to be provided in Enum's definition inside a migration.
To Reproduce
sql_types.py
tables.py
alembic revision --autogenerate -m "Enums"
provides (shortened):Error As you can see from the migration enums are defined differently - the second one misses its
name
property. If I try to run it on PostgreSQL I get the error:Stacktrace
```cmd INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Generating static SQL INFO [alembic.runtime.migration] Will assume transactional DDL. BEGIN; CREATE TABLE alembic_version ( version_num VARCHAR(32) NOT NULL, CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num) ); INFO [alembic.runtime.migration] Running upgrade -> 264b6f57ae76, Enums -- Running upgrade -> 264b6f57ae76 CREATE TYPE testenum AS ENUM ('FIRST', 'SECOND'); Traceback (most recent call last): File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\runpy.py", line 196, in _run_module_as_main return _run_code(code, main_globals, None, File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\runpy.py", line 86, in _run_code exec(code, run_globals) File "C:\Users\Михаил\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\alembic\__main__.py", line 4, inVersions.
Additional context The docs state that subclassing from
TypeDecorator
should be preferred:Have a nice day!