summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2020-02-29 14:40:45 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2020-03-02 17:24:19 -0500
commit57dc36a01b2b334a996f73f6a78b3bfbe4d9f2ec (patch)
tree77cbb0199ca91be3b0816e3a5bd4c217e36a7d1b /lib/sqlalchemy/sql
parent649de79950dcf952d7a44069faf36925c23c4e63 (diff)
downloadsqlalchemy-57dc36a01b2b334a996f73f6a78b3bfbe4d9f2ec.tar.gz
Ensure all nested exception throws have a cause
Applied an explicit "cause" to most if not all internally raised exceptions that are raised from within an internal exception catch, to avoid misleading stacktraces that suggest an error within the handling of an exception. While it would be preferable to suppress the internally caught exception in the way that the ``__suppress_context__`` attribute would, there does not as yet seem to be a way to do this without suppressing an enclosing user constructed context, so for now it exposes the internally caught exception as the cause so that full information about the context of the error is maintained. Fixes: #4849 Change-Id: I55a86b29023675d9e5e49bc7edc5a2dc0bcd4751
Diffstat (limited to 'lib/sqlalchemy/sql')
-rw-r--r--lib/sqlalchemy/sql/base.py25
-rw-r--r--lib/sqlalchemy/sql/coercions.py39
-rw-r--r--lib/sqlalchemy/sql/compiler.py15
-rw-r--r--lib/sqlalchemy/sql/ddl.py5
-rw-r--r--lib/sqlalchemy/sql/elements.py66
-rw-r--r--lib/sqlalchemy/sql/schema.py25
-rw-r--r--lib/sqlalchemy/sql/selectable.py22
-rw-r--r--lib/sqlalchemy/sql/sqltypes.py18
-rw-r--r--lib/sqlalchemy/sql/type_api.py44
-rw-r--r--lib/sqlalchemy/sql/visitors.py7
10 files changed, 168 insertions, 98 deletions
diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py
index a7324c45f..2d336360f 100644
--- a/lib/sqlalchemy/sql/base.py
+++ b/lib/sqlalchemy/sql/base.py
@@ -128,8 +128,8 @@ class _DialectArgView(util.collections_abc.MutableMapping):
def _key(self, key):
try:
dialect, value_key = key.split("_", 1)
- except ValueError:
- raise KeyError(key)
+ except ValueError as err:
+ util.raise_(KeyError(key), replace_context=err)
else:
return dialect, value_key
@@ -138,17 +138,20 @@ class _DialectArgView(util.collections_abc.MutableMapping):
try:
opt = self.obj.dialect_options[dialect]
- except exc.NoSuchModuleError:
- raise KeyError(key)
+ except exc.NoSuchModuleError as err:
+ util.raise_(KeyError(key), replace_context=err)
else:
return opt[value_key]
def __setitem__(self, key, value):
try:
dialect, value_key = self._key(key)
- except KeyError:
- raise exc.ArgumentError(
- "Keys must be of the form <dialectname>_<argname>"
+ except KeyError as err:
+ util.raise_(
+ exc.ArgumentError(
+ "Keys must be of the form <dialectname>_<argname>"
+ ),
+ replace_context=err,
)
else:
self.obj.dialect_options[dialect][value_key] = value
@@ -634,17 +637,17 @@ class ColumnCollection(object):
def __getitem__(self, key):
try:
return self._index[key]
- except KeyError:
+ except KeyError as err:
if isinstance(key, util.int_types):
- raise IndexError(key)
+ util.raise_(IndexError(key), replace_context=err)
else:
raise
def __getattr__(self, key):
try:
return self._index[key]
- except KeyError:
- raise AttributeError(key)
+ except KeyError as err:
+ util.raise_(AttributeError(key), replace_context=err)
def __contains__(self, key):
if key not in self._index:
diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py
index b3bf4e93b..fc841bb4b 100644
--- a/lib/sqlalchemy/sql/coercions.py
+++ b/lib/sqlalchemy/sql/coercions.py
@@ -133,7 +133,13 @@ class RoleImpl(object):
self._raise_for_expected(element, argname, resolved)
def _raise_for_expected(
- self, element, argname=None, resolved=None, advice=None, code=None
+ self,
+ element,
+ argname=None,
+ resolved=None,
+ advice=None,
+ code=None,
+ err=None,
):
if argname:
msg = "%s expected for argument %r; got %r." % (
@@ -147,7 +153,7 @@ class RoleImpl(object):
if advice:
msg += " " + advice
- raise exc.ArgumentError(msg, code=code)
+ util.raise_(exc.ArgumentError(msg, code=code), replace_context=err)
class _Deannotate(object):
@@ -201,16 +207,19 @@ class _ColumnCoercions(object):
def _no_text_coercion(
- element, argname=None, exc_cls=exc.ArgumentError, extra=None
+ element, argname=None, exc_cls=exc.ArgumentError, extra=None, err=None
):
- raise exc_cls(
- "%(extra)sTextual SQL expression %(expr)r %(argname)sshould be "
- "explicitly declared as text(%(expr)r)"
- % {
- "expr": util.ellipses_string(element),
- "argname": "for argument %s" % (argname,) if argname else "",
- "extra": "%s " % extra if extra else "",
- }
+ util.raise_(
+ exc_cls(
+ "%(extra)sTextual SQL expression %(expr)r %(argname)sshould be "
+ "explicitly declared as text(%(expr)r)"
+ % {
+ "expr": util.ellipses_string(element),
+ "argname": "for argument %s" % (argname,) if argname else "",
+ "extra": "%s " % extra if extra else "",
+ }
+ ),
+ replace_context=err,
)
@@ -290,8 +299,8 @@ class ExpressionElementImpl(
return elements.BindParameter(
name, element, type_, unique=True
)
- except exc.ArgumentError:
- self._raise_for_expected(element)
+ except exc.ArgumentError as err:
+ self._raise_for_expected(element, err=err)
class BinaryElementImpl(
@@ -302,8 +311,8 @@ class BinaryElementImpl(
):
try:
return expr._bind_param(operator, element, type_=bindparam_type)
- except exc.ArgumentError:
- self._raise_for_expected(element)
+ except exc.ArgumentError as err:
+ self._raise_for_expected(element, err=err)
def _post_coercion(self, resolved, expr, **kw):
if (
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py
index 9c1f50ce1..d31cf67f8 100644
--- a/lib/sqlalchemy/sql/compiler.py
+++ b/lib/sqlalchemy/sql/compiler.py
@@ -1074,7 +1074,7 @@ class SQLCompiler(Compiled):
col = only_froms[element.element]
else:
col = with_cols[element.element]
- except KeyError:
+ except KeyError as err:
coercions._no_text_coercion(
element.element,
extra=(
@@ -1082,6 +1082,7 @@ class SQLCompiler(Compiled):
"GROUP BY / DISTINCT etc."
),
exc_cls=exc.CompileError,
+ err=err,
)
else:
kwargs["render_label_as_label"] = col
@@ -1671,8 +1672,11 @@ class SQLCompiler(Compiled):
else:
try:
opstring = OPERATORS[operator_]
- except KeyError:
- raise exc.UnsupportedCompilationError(self, operator_)
+ except KeyError as err:
+ util.raise_(
+ exc.UnsupportedCompilationError(self, operator_),
+ replace_context=err,
+ )
else:
return self._generate_generic_binary(
binary, opstring, from_linter=from_linter, **kw
@@ -3286,11 +3290,12 @@ class DDLCompiler(Compiled):
if column.primary_key:
first_pk = True
except exc.CompileError as ce:
- util.raise_from_cause(
+ util.raise_(
exc.CompileError(
util.u("(in table '%s', column '%s'): %s")
% (table.description, column.name, ce.args[0])
- )
+ ),
+ from_=ce,
)
const = self.create_table_constraints(
diff --git a/lib/sqlalchemy/sql/ddl.py b/lib/sqlalchemy/sql/ddl.py
index 31bcc34a4..5a2095604 100644
--- a/lib/sqlalchemy/sql/ddl.py
+++ b/lib/sqlalchemy/sql/ddl.py
@@ -801,7 +801,7 @@ class SchemaDropper(DDLBase):
)
collection = [(t, ()) for t in unsorted_tables]
else:
- util.raise_from_cause(
+ util.raise_(
exc.CircularDependencyError(
err2.args[0],
err2.cycles,
@@ -818,7 +818,8 @@ class SchemaDropper(DDLBase):
sorted([t.fullname for t in err2.cycles])
)
),
- )
+ ),
+ from_=err2,
)
seq_coll = [
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index df690c383..d0babb1be 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -747,10 +747,13 @@ class ColumnElement(
def comparator(self):
try:
comparator_factory = self.type.comparator_factory
- except AttributeError:
- raise TypeError(
- "Object %r associated with '.type' attribute "
- "is not a TypeEngine class or object" % self.type
+ except AttributeError as err:
+ util.raise_(
+ TypeError(
+ "Object %r associated with '.type' attribute "
+ "is not a TypeEngine class or object" % self.type
+ ),
+ replace_context=err,
)
else:
return comparator_factory(self)
@@ -758,10 +761,17 @@ class ColumnElement(
def __getattr__(self, key):
try:
return getattr(self.comparator, key)
- except AttributeError:
- raise AttributeError(
- "Neither %r object nor %r object has an attribute %r"
- % (type(self).__name__, type(self.comparator).__name__, key)
+ except AttributeError as err:
+ util.raise_(
+ AttributeError(
+ "Neither %r object nor %r object has an attribute %r"
+ % (
+ type(self).__name__,
+ type(self.comparator).__name__,
+ key,
+ )
+ ),
+ replace_context=err,
)
def operate(self, op, *other, **kwargs):
@@ -1742,10 +1752,13 @@ class TextClause(
# a unique/anonymous key in any case, so use the _orig_key
# so that a text() construct can support unique parameters
existing = new_params[bind._orig_key]
- except KeyError:
- raise exc.ArgumentError(
- "This text() construct doesn't define a "
- "bound parameter named %r" % bind._orig_key
+ except KeyError as err:
+ util.raise_(
+ exc.ArgumentError(
+ "This text() construct doesn't define a "
+ "bound parameter named %r" % bind._orig_key
+ ),
+ replace_context=err,
)
else:
new_params[existing._orig_key] = bind
@@ -1753,10 +1766,13 @@ class TextClause(
for key, value in names_to_values.items():
try:
existing = new_params[key]
- except KeyError:
- raise exc.ArgumentError(
- "This text() construct doesn't define a "
- "bound parameter named %r" % key
+ except KeyError as err:
+ util.raise_(
+ exc.ArgumentError(
+ "This text() construct doesn't define a "
+ "bound parameter named %r" % key
+ ),
+ replace_context=err,
)
else:
new_params[key] = existing._with_value(value)
@@ -3665,9 +3681,12 @@ class Over(ColumnElement):
else:
try:
lower = int(range_[0])
- except ValueError:
- raise exc.ArgumentError(
- "Integer or None expected for range value"
+ except ValueError as err:
+ util.raise_(
+ exc.ArgumentError(
+ "Integer or None expected for range value"
+ ),
+ replace_context=err,
)
else:
if lower == 0:
@@ -3678,9 +3697,12 @@ class Over(ColumnElement):
else:
try:
upper = int(range_[1])
- except ValueError:
- raise exc.ArgumentError(
- "Integer or None expected for range value"
+ except ValueError as err:
+ util.raise_(
+ exc.ArgumentError(
+ "Integer or None expected for range value"
+ ),
+ replace_context=err,
)
else:
if upper == 0:
diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py
index e6d3a6b05..5445a1bce 100644
--- a/lib/sqlalchemy/sql/schema.py
+++ b/lib/sqlalchemy/sql/schema.py
@@ -107,12 +107,13 @@ class SchemaItem(SchemaEventTarget, visitors.Visitable):
if item is not None:
try:
spwd = item._set_parent_with_dispatch
- except AttributeError:
- util.raise_from_cause(
+ except AttributeError as err:
+ util.raise_(
exc.ArgumentError(
"'SchemaItem' object, such as a 'Column' or a "
"'Constraint' expected, got %r" % item
- )
+ ),
+ replace_context=err,
)
else:
spwd(self)
@@ -1569,15 +1570,16 @@ class Column(DialectKWArgs, SchemaItem, ColumnClause):
_proxies=[self],
*fk
)
- except TypeError:
- util.raise_from_cause(
+ except TypeError as err:
+ util.raise_(
TypeError(
"Could not create a copy of this %r object. "
"Ensure the class includes a _constructor() "
"attribute or method which accepts the "
"standard Column constructor arguments, or "
"references the Column class itself." % self.__class__
- )
+ ),
+ from_=err,
)
c.table = selectable
@@ -3187,10 +3189,13 @@ class ForeignKeyConstraint(ColumnCollectionConstraint):
try:
ColumnCollectionConstraint._set_parent(self, table)
except KeyError as ke:
- raise exc.ArgumentError(
- "Can't create ForeignKeyConstraint "
- "on table '%s': no column "
- "named '%s' is present." % (table.description, ke.args[0])
+ util.raise_(
+ exc.ArgumentError(
+ "Can't create ForeignKeyConstraint "
+ "on table '%s': no column "
+ "named '%s' is present." % (table.description, ke.args[0])
+ ),
+ from_=ke,
)
for col, fk in zip(self.columns, self.elements):
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py
index b8d88e160..b972c13be 100644
--- a/lib/sqlalchemy/sql/selectable.py
+++ b/lib/sqlalchemy/sql/selectable.py
@@ -2620,10 +2620,13 @@ class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase):
return None
try:
value = clause._limit_offset_value
- except AttributeError:
- raise exc.CompileError(
- "This SELECT structure does not use a simple "
- "integer value for %s" % attrname
+ except AttributeError as err:
+ util.raise_(
+ exc.CompileError(
+ "This SELECT structure does not use a simple "
+ "integer value for %s" % attrname
+ ),
+ replace_context=err,
)
else:
return util.asint(value)
@@ -3489,10 +3492,13 @@ class Select(
try:
cols_present = bool(columns)
- except TypeError:
- raise exc.ArgumentError(
- "columns argument to select() must "
- "be a Python list or other iterable"
+ except TypeError as err:
+ util.raise_(
+ exc.ArgumentError(
+ "columns argument to select() must "
+ "be a Python list or other iterable"
+ ),
+ from_=err,
)
if cols_present:
diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py
index 22c80cc91..e4a029a3e 100644
--- a/lib/sqlalchemy/sql/sqltypes.py
+++ b/lib/sqlalchemy/sql/sqltypes.py
@@ -1462,7 +1462,7 @@ class Enum(Emulated, String, SchemaType):
def _db_value_for_elem(self, elem):
try:
return self._valid_lookup[elem]
- except KeyError:
+ except KeyError as err:
# for unknown string values, we return as is. While we can
# validate these if we wanted, that does not allow for lesser-used
# end-user use cases, such as using a LIKE comparison with an enum,
@@ -1476,8 +1476,11 @@ class Enum(Emulated, String, SchemaType):
):
return elem
else:
- raise LookupError(
- '"%s" is not among the defined enum values' % elem
+ util.raise_(
+ LookupError(
+ '"%s" is not among the defined enum values' % elem
+ ),
+ replace_context=err,
)
class Comparator(String.Comparator):
@@ -1496,9 +1499,12 @@ class Enum(Emulated, String, SchemaType):
def _object_value_for_elem(self, elem):
try:
return self._object_lookup[elem]
- except KeyError:
- raise LookupError(
- '"%s" is not among the defined enum values' % elem
+ except KeyError as err:
+ util.raise_(
+ LookupError(
+ '"%s" is not among the defined enum values' % elem
+ ),
+ replace_context=err,
)
def __repr__(self):
diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py
index c6c860844..739f96195 100644
--- a/lib/sqlalchemy/sql/type_api.py
+++ b/lib/sqlalchemy/sql/type_api.py
@@ -479,9 +479,12 @@ class TypeEngine(Traversible):
try:
return dialect._type_memos[self]["literal"]
except KeyError:
- d = self._dialect_info(dialect)
- d["literal"] = lp = d["impl"].literal_processor(dialect)
- return lp
+ pass
+ # avoid KeyError context coming into literal_processor() function
+ # raises
+ d = self._dialect_info(dialect)
+ d["literal"] = lp = d["impl"].literal_processor(dialect)
+ return lp
def _cached_bind_processor(self, dialect):
"""Return a dialect-specific bind processor for this type."""
@@ -489,9 +492,12 @@ class TypeEngine(Traversible):
try:
return dialect._type_memos[self]["bind"]
except KeyError:
- d = self._dialect_info(dialect)
- d["bind"] = bp = d["impl"].bind_processor(dialect)
- return bp
+ pass
+ # avoid KeyError context coming into bind_processor() function
+ # raises
+ d = self._dialect_info(dialect)
+ d["bind"] = bp = d["impl"].bind_processor(dialect)
+ return bp
def _cached_result_processor(self, dialect, coltype):
"""Return a dialect-specific result processor for this type."""
@@ -499,21 +505,27 @@ class TypeEngine(Traversible):
try:
return dialect._type_memos[self][coltype]
except KeyError:
- d = self._dialect_info(dialect)
- # key assumption: DBAPI type codes are
- # constants. Else this dictionary would
- # grow unbounded.
- d[coltype] = rp = d["impl"].result_processor(dialect, coltype)
- return rp
+ pass
+ # avoid KeyError context coming into result_processor() function
+ # raises
+ d = self._dialect_info(dialect)
+ # key assumption: DBAPI type codes are
+ # constants. Else this dictionary would
+ # grow unbounded.
+ d[coltype] = rp = d["impl"].result_processor(dialect, coltype)
+ return rp
def _cached_custom_processor(self, dialect, key, fn):
try:
return dialect._type_memos[self][key]
except KeyError:
- d = self._dialect_info(dialect)
- impl = d["impl"]
- d[key] = result = fn(impl)
- return result
+ pass
+ # avoid KeyError context coming into fn() function
+ # raises
+ d = self._dialect_info(dialect)
+ impl = d["impl"]
+ d[key] = result = fn(impl)
+ return result
def _dialect_info(self, dialect):
"""Return a dialect-specific registry which
diff --git a/lib/sqlalchemy/sql/visitors.py b/lib/sqlalchemy/sql/visitors.py
index 77e6b53a8..fda48c657 100644
--- a/lib/sqlalchemy/sql/visitors.py
+++ b/lib/sqlalchemy/sql/visitors.py
@@ -62,9 +62,10 @@ def _generate_compiler_dispatch(cls):
"def _compiler_dispatch(self, visitor, **kw):\n"
" try:\n"
" meth = visitor.visit_%(name)s\n"
- " except AttributeError:\n"
- " util.raise_from_cause(\n"
- " exc.UnsupportedCompilationError(visitor, cls))\n"
+ " except AttributeError as err:\n"
+ " util.raise_(\n"
+ " exc.UnsupportedCompilationError(visitor, cls), \n"
+ " replace_context=err)\n"
" else:\n"
" return meth(self, **kw)\n"
) % {"name": visit_name}