diff options
| author | Gord Thompson <gord@gordthompson.com> | 2020-12-07 18:37:29 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2020-12-08 19:54:05 -0500 |
| commit | b71e46f0470964358d44aec08f940260f78691f0 (patch) | |
| tree | 44c9b5934ad550b4688700e8124204411e42190f /lib/sqlalchemy | |
| parent | 18b1b261ff988549e75b011f2f4296fb13b24d64 (diff) | |
| download | sqlalchemy-b71e46f0470964358d44aec08f940260f78691f0.tar.gz | |
Implement `TypeEngine.as_generic`
Added :meth:`_types.TypeEngine.as_generic` to map dialect-specific types,
such as :class:`sqlalchemy.dialects.mysql.INTEGER`, with the "best match"
generic SQLAlchemy type, in this case :class:`_types.Integer`. Pull
request courtesy Andrew Hannigan.
Abstract away how we check for "overridden methods" so it is more
clear what the intent is and that the methodology can be
independently tested.
Fixes: #5659
Closes: #5714
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/5714
Pull-request-sha: 91afb9a0ba3bfa81a1ded80c025989213cf6e4eb
Change-Id: Ic54d6690ecc10dc69e6e72856d5620036cea472a
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/dialects/oracle/base.py | 7 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/postgresql/base.py | 3 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/events.py | 3 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/sqltypes.py | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/type_api.py | 101 | ||||
| -rw-r--r-- | lib/sqlalchemy/util/__init__.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/util/langhelpers.py | 15 |
7 files changed, 111 insertions, 31 deletions
diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py index 223c1db98..371a6702e 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -666,6 +666,13 @@ class INTERVAL(sqltypes.NativeForEmulated, sqltypes._AbstractInterval): def _type_affinity(self): return sqltypes.Interval + def as_generic(self, allow_nulltype=False): + return sqltypes.Interval( + native=True, + second_precision=self.second_precision, + day_precision=self.day_precision, + ) + class ROWID(sqltypes.TypeEngine): """Oracle ROWID type. diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 612bc9223..e41e489c0 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1474,6 +1474,9 @@ class INTERVAL(sqltypes.NativeForEmulated, sqltypes._AbstractInterval): def _type_affinity(self): return sqltypes.Interval + def as_generic(self, allow_nulltype=False): + return sqltypes.Interval(native=True, second_precision=self.precision) + @property def python_type(self): return dt.timedelta diff --git a/lib/sqlalchemy/sql/events.py b/lib/sqlalchemy/sql/events.py index 58d04f7aa..797ca697f 100644 --- a/lib/sqlalchemy/sql/events.py +++ b/lib/sqlalchemy/sql/events.py @@ -314,4 +314,7 @@ class DDLEvents(event.Events): :ref:`automap_intercepting_columns` - in the :ref:`automap_toplevel` documentation + :ref:`metadata_reflection_dbagnostic_types` - in + the :ref:`metadata_reflection_toplevel` documentation + """ diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 45d4f0b7f..581573d17 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -1607,6 +1607,18 @@ class Enum(Emulated, String, SchemaType): to_inspect=[Enum, SchemaType], ) + def as_generic(self, allow_nulltype=False): + if hasattr(self, "enums"): + args = self.enums + else: + raise NotImplementedError( + "TypeEngine.as_generic() heuristic " + "is undefined for types that inherit Enum but do not have " + "an `enums` attribute." + ) + + return util.constructor_copy(self, self._generic_type_affinity, *args) + def adapt_to_emulated(self, impltype, **kw): kw.setdefault("_expect_unicode", self._expect_unicode) kw.setdefault("validate_strings", self.validate_strings) diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py index bca6e9020..b48886cca 100644 --- a/lib/sqlalchemy/sql/type_api.py +++ b/lib/sqlalchemy/sql/type_api.py @@ -17,7 +17,6 @@ from .visitors import TraversibleType from .. import exc from .. import util - # these are back-assigned by sqltypes. BOOLEANTYPE = None INTEGERTYPE = None @@ -372,10 +371,7 @@ class TypeEngine(Traversible): """ - return ( - self.__class__.bind_expression.__code__ - is not TypeEngine.bind_expression.__code__ - ) + return util.method_is_overridden(self, TypeEngine.bind_expression) @staticmethod def _to_instance(cls_or_self): @@ -456,12 +452,13 @@ class TypeEngine(Traversible): else: return self.__class__ - @classmethod - def _is_generic_type(cls): - n = cls.__name__ - return n.upper() != n - + @util.memoized_property def _generic_type_affinity(self): + best_camelcase = None + best_uppercase = None + + if not isinstance(self, (TypeEngine, UserDefinedType)): + return self.__class__ for t in self.__class__.__mro__: if ( @@ -470,13 +467,56 @@ class TypeEngine(Traversible): "sqlalchemy.sql.sqltypes", "sqlalchemy.sql.type_api", ) - and t._is_generic_type() + and issubclass(t, TypeEngine) + and t is not TypeEngine + and t.__name__[0] != "_" ): - if t in (TypeEngine, UserDefinedType): - return NULLTYPE.__class__ - return t - else: - return self.__class__ + if t.__name__.isupper() and not best_uppercase: + best_uppercase = t + elif not t.__name__.isupper() and not best_camelcase: + best_camelcase = t + + return best_camelcase or best_uppercase or NULLTYPE.__class__ + + def as_generic(self, allow_nulltype=False): + """ + Return an instance of the generic type corresponding to this type + using heuristic rule. The method may be overridden if this + heuristic rule is not sufficient. + + >>> from sqlalchemy.dialects.mysql import INTEGER + >>> INTEGER(display_width=4).as_generic() + Integer() + + >>> from sqlalchemy.dialects.mysql import NVARCHAR + >>> NVARCHAR(length=100).as_generic() + Unicode(length=100) + + .. versionadded:: 1.4.0b2 + + + .. seealso:: + + :ref:`metadata_reflection_dbagnostic_types` - describes the + use of :meth:`_types.TypeEngine.as_generic` in conjunction with + the :meth:`_sql.DDLEvents.column_reflect` event, which is its + intended use. + + """ + if ( + not allow_nulltype + and self._generic_type_affinity == NULLTYPE.__class__ + ): + raise NotImplementedError( + "Default TypeEngine.as_generic() " + "heuristic method was unsuccessful for {}. A custom " + "as_generic() method must be implemented for this " + "type class.".format( + self.__class__.__module__ + "." + self.__class__.__name__ + ) + ) + + return util.constructor_copy(self, self._generic_type_affinity) def dialect_impl(self, dialect): """Return a dialect-specific implementation for this @@ -1171,18 +1211,16 @@ class TypeDecorator(SchemaEventTarget, TypeEngine): """ - return ( - self.__class__.process_bind_param.__code__ - is not TypeDecorator.process_bind_param.__code__ + return util.method_is_overridden( + self, TypeDecorator.process_bind_param ) @util.memoized_property def _has_literal_processor(self): """memoized boolean, check if process_literal_param is implemented.""" - return ( - self.__class__.process_literal_param.__code__ - is not TypeDecorator.process_literal_param.__code__ + return util.method_is_overridden( + self, TypeDecorator.process_literal_param ) def literal_processor(self, dialect): @@ -1278,9 +1316,9 @@ class TypeDecorator(SchemaEventTarget, TypeEngine): exception throw. """ - return ( - self.__class__.process_result_value.__code__ - is not TypeDecorator.process_result_value.__code__ + + return util.method_is_overridden( + self, TypeDecorator.process_result_value ) def result_processor(self, dialect, coltype): @@ -1322,10 +1360,11 @@ class TypeDecorator(SchemaEventTarget, TypeEngine): @util.memoized_property def _has_bind_expression(self): + return ( - self.__class__.bind_expression.__code__ - is not TypeDecorator.bind_expression.__code__ - ) or self.impl._has_bind_expression + util.method_is_overridden(self, TypeDecorator.bind_expression) + or self.impl._has_bind_expression + ) def bind_expression(self, bindparam): return self.impl.bind_expression(bindparam) @@ -1340,9 +1379,9 @@ class TypeDecorator(SchemaEventTarget, TypeEngine): """ return ( - self.__class__.column_expression.__code__ - is not TypeDecorator.column_expression.__code__ - ) or self.impl._has_column_expression + util.method_is_overridden(self, TypeDecorator.column_expression) + or self.impl._has_column_expression + ) def column_expression(self, column): return self.impl.column_expression(column) diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index 2db1adb8d..f4363d03c 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -147,6 +147,7 @@ from .langhelpers import md5_hex # noqa from .langhelpers import memoized_instancemethod # noqa from .langhelpers import memoized_property # noqa from .langhelpers import MemoizedSlots # noqa +from .langhelpers import method_is_overridden # noqa from .langhelpers import methods_equivalent # noqa from .langhelpers import monkeypatch_proxied_specials # noqa from .langhelpers import NoneType # noqa diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 8d6c2d8ee..b0963ce43 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -114,6 +114,21 @@ def clsname_as_plain_name(cls): ) +def method_is_overridden(instance_or_cls, against_method): + """Return True if the two class methods don't match.""" + + if not isinstance(instance_or_cls, type): + current_cls = instance_or_cls.__class__ + else: + current_cls = instance_or_cls + + method_name = against_method.__name__ + + current_method = getattr(current_cls, method_name) + + return current_method != against_method + + def decode_slice(slc): """decode a slice object as sent to __getitem__. |
