summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorGord Thompson <gord@gordthompson.com>2020-12-07 18:37:29 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2020-12-08 19:54:05 -0500
commitb71e46f0470964358d44aec08f940260f78691f0 (patch)
tree44c9b5934ad550b4688700e8124204411e42190f /lib/sqlalchemy
parent18b1b261ff988549e75b011f2f4296fb13b24d64 (diff)
downloadsqlalchemy-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.py7
-rw-r--r--lib/sqlalchemy/dialects/postgresql/base.py3
-rw-r--r--lib/sqlalchemy/sql/events.py3
-rw-r--r--lib/sqlalchemy/sql/sqltypes.py12
-rw-r--r--lib/sqlalchemy/sql/type_api.py101
-rw-r--r--lib/sqlalchemy/util/__init__.py1
-rw-r--r--lib/sqlalchemy/util/langhelpers.py15
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__.