From: Mike Bayer Date: Wed, 21 Aug 2019 21:29:55 +0000 (-0400) Subject: Add hide_parameters flag to create_engine X-Git-Tag: rel_1_4_0b1~750^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4b321e8a5e6b728a818a801c3ad90bb759c584bc;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add hide_parameters flag to create_engine Added new parameter :paramref:`.create_engine.hide_parameters` which when set to True will cause SQL parameters to no longer be logged, nor rendered in the string representation of a :class:`.StatementError` object. Fixes: #4815 Change-Id: Ib87f868b6936cf6b42b192644e9d732ec24266c2 --- diff --git a/doc/build/changelog/unreleased_13/4815.rst b/doc/build/changelog/unreleased_13/4815.rst new file mode 100644 index 0000000000..5157f99e7f --- /dev/null +++ b/doc/build/changelog/unreleased_13/4815.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: feature, engine + :tickets: 4815 + + Added new parameter :paramref:`.create_engine.hide_parameters` which when + set to True will cause SQL parameters to no longer be logged, nor rendered + in the string representation of a :class:`.StatementError` object. + diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index a708643b31..a0a2680ca0 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1222,9 +1222,14 @@ class Connection(Connectable): if self._echo: self.engine.logger.info(statement) - self.engine.logger.info( - "%r", sql_util._repr_params(parameters, batches=10) - ) + if not self.engine.hide_parameters: + self.engine.logger.info( + "%r", sql_util._repr_params(parameters, batches=10) + ) + else: + self.engine.logger.info( + "[SQL parameters hidden due to hide_parameters=True]" + ) evt_handled = False try: @@ -1388,6 +1393,7 @@ class Connection(Connectable): parameters, e, self.dialect.dbapi.Error, + hide_parameters=self.engine.hide_parameters, dialect=self.dialect, ), exc_info, @@ -1408,6 +1414,7 @@ class Connection(Connectable): parameters, e, self.dialect.dbapi.Error, + hide_parameters=self.engine.hide_parameters, connection_invalidated=self._is_disconnect, dialect=self.dialect, ) @@ -1508,6 +1515,7 @@ class Connection(Connectable): None, e, dialect.dbapi.Error, + hide_parameters=engine.hide_parameters, connection_invalidated=is_disconnect, ) else: @@ -1904,6 +1912,7 @@ class Engine(Connectable, log.Identified): echo=None, proxy=None, execution_options=None, + hide_parameters=False, ): self.pool = pool self.url = url @@ -1911,6 +1920,7 @@ class Engine(Connectable, log.Identified): if logging_name: self.logging_name = logging_name self.echo = echo + self.hide_parameters = hide_parameters log.instance_logger(self, echoflag=echo) if proxy: interfaces.ConnectionProxy._adapt_listener(self, proxy) @@ -2339,6 +2349,7 @@ class OptionEngine(Engine): self.dialect = proxied.dialect self.logging_name = proxied.logging_name self.echo = proxied.echo + self.hide_parameters = proxied.hide_parameters log.instance_logger(self, echoflag=self.echo) # note: this will propagate events that are assigned to the parent diff --git a/lib/sqlalchemy/engine/create.py b/lib/sqlalchemy/engine/create.py index 72be6009bd..43378f24e0 100644 --- a/lib/sqlalchemy/engine/create.py +++ b/lib/sqlalchemy/engine/create.py @@ -197,6 +197,12 @@ def create_engine(url, **kwargs): be applied to all connections. See :meth:`~sqlalchemy.engine.Connection.execution_options` + :param hide_parameters: Boolean, when set to True, SQL statement parameters + will not be displayed in INFO logging nor will they be formatted into + the string representation of :class:`.StatementError` objects. + + .. versionadded:: 1.3.8 + :param implicit_returning=True: When ``True``, a RETURNING- compatible construct, if available, will be used to fetch newly generated primary key values when a single row diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index 1e575626a5..efee58d991 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -332,11 +332,20 @@ class StatementError(SQLAlchemyError): orig = None """The DBAPI exception object.""" - def __init__(self, message, statement, params, orig, code=None): + def __init__( + self, + message, + statement, + params, + orig, + hide_parameters=False, + code=None, + ): SQLAlchemyError.__init__(self, message, code=code) self.statement = statement self.params = params self.orig = orig + self.hide_parameters = hide_parameters self.detail = [] def add_detail(self, msg): @@ -345,7 +354,13 @@ class StatementError(SQLAlchemyError): def __reduce__(self): return ( self.__class__, - (self.args[0], self.statement, self.params, self.orig), + ( + self.args[0], + self.statement, + self.params, + self.orig, + self.hide_parameters, + ), ) def _sql_message(self, as_unicode): @@ -361,8 +376,13 @@ class StatementError(SQLAlchemyError): stmt_detail = "[SQL: %s]" % self.statement details.append(stmt_detail) if self.params: - params_repr = util._repr_params(self.params, 10) - details.append("[parameters: %r]" % params_repr) + if self.hide_parameters: + details.append( + "[SQL parameters hidden due to hide_parameters=True]" + ) + else: + params_repr = util._repr_params(self.params, 10) + details.append("[parameters: %r]" % params_repr) code_str = self._code_str() if code_str: details.append(code_str) @@ -401,6 +421,7 @@ class DBAPIError(StatementError): params, orig, dbapi_base_err, + hide_parameters=False, connection_invalidated=False, dialect=None, ): @@ -425,6 +446,7 @@ class DBAPIError(StatementError): statement, params, orig, + hide_parameters=hide_parameters, code=orig.code, ) elif not isinstance(orig, dbapi_base_err) and statement: @@ -438,6 +460,7 @@ class DBAPIError(StatementError): statement, params, orig, + hide_parameters=hide_parameters, ) glob = globals() @@ -452,7 +475,12 @@ class DBAPIError(StatementError): break return cls( - statement, params, orig, connection_invalidated, code=cls.code + statement, + params, + orig, + connection_invalidated=connection_invalidated, + hide_parameters=hide_parameters, + code=cls.code, ) def __reduce__(self): @@ -462,12 +490,19 @@ class DBAPIError(StatementError): self.statement, self.params, self.orig, + self.hide_parameters, self.connection_invalidated, ), ) def __init__( - self, statement, params, orig, connection_invalidated=False, code=None + self, + statement, + params, + orig, + hide_parameters=False, + connection_invalidated=False, + code=None, ): try: text = str(orig) @@ -480,6 +515,7 @@ class DBAPIError(StatementError): statement, params, orig, + hide_parameters, code=code, ) self.connection_invalidated = connection_invalidated diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index 424631fe38..652cea3f35 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -522,10 +522,14 @@ class ExecuteTest(fixtures.TestBase): def _test_stmt_exception_pickleable(self, orig): for sa_exc in ( tsa.exc.StatementError( - "some error", "select * from table", {"foo": "bar"}, orig + "some error", + "select * from table", + {"foo": "bar"}, + orig, + False, ), tsa.exc.InterfaceError( - "select * from table", {"foo": "bar"}, orig + "select * from table", {"foo": "bar"}, orig, True ), tsa.exc.NoReferencedTableError("message", "tname"), tsa.exc.NoReferencedColumnError("message", "tname", "cname"), diff --git a/test/engine/test_logging.py b/test/engine/test_logging.py index bd425c940d..d55f4249a0 100644 --- a/test/engine/test_logging.py +++ b/test/engine/test_logging.py @@ -1,7 +1,13 @@ import logging.handlers import sqlalchemy as tsa +from sqlalchemy import bindparam +from sqlalchemy import Column +from sqlalchemy import MetaData +from sqlalchemy import or_ from sqlalchemy import select +from sqlalchemy import String +from sqlalchemy import Table from sqlalchemy import util from sqlalchemy.testing import assert_raises_message from sqlalchemy.testing import engines @@ -18,7 +24,11 @@ class LogParamsTest(fixtures.TestBase): def setup(self): self.eng = engines.testing_engine(options={"echo": True}) + self.no_param_engine = engines.testing_engine( + options={"echo": True, "hide_parameters": True} + ) self.eng.execute("create table foo (data string)") + self.no_param_engine.execute("create table foo (data string)") self.buf = logging.handlers.BufferingHandler(100) for log in [logging.getLogger("sqlalchemy.engine")]: log.addHandler(self.buf) @@ -41,6 +51,16 @@ class LogParamsTest(fixtures.TestBase): "parameter sets ... {'data': '98'}, {'data': '99'}]", ) + def test_log_no_parameters(self): + self.no_param_engine.execute( + "INSERT INTO foo (data) values (:data)", + [{"data": str(i)} for i in range(100)], + ) + eq_( + self.buf.buffer[1].message, + "[SQL parameters hidden due to hide_parameters=True]", + ) + def test_log_large_list(self): self.eng.execute( "INSERT INTO foo (data) values (?)", @@ -106,6 +126,48 @@ class LogParamsTest(fixtures.TestBase): r"\(.*.NoneType\) None\n\[SQL: foo\]\n\[parameters: {'x': 'y'}\]", ) + def test_exception_format_hide_parameters(self): + exception = tsa.exc.IntegrityError( + "foo", {"x": "y"}, None, hide_parameters=True + ) + eq_regex( + str(exception), + r"\(.*.NoneType\) None\n\[SQL: foo\]\n" + r"\[SQL parameters hidden due to hide_parameters=True\]", + ) + + def test_exception_format_hide_parameters_dbapi_round_trip(self): + assert_raises_message( + tsa.exc.DBAPIError, + r".*INSERT INTO nonexistent \(data\) values \(:data\)\]\n" + r"\[SQL parameters hidden due to hide_parameters=True\]", + lambda: self.no_param_engine.execute( + "INSERT INTO nonexistent (data) values (:data)", + [{"data": str(i)} for i in range(10)], + ), + ) + + def test_exception_format_hide_parameters_nondbapi_round_trip(self): + foo = Table("foo", MetaData(), Column("data", String)) + + with self.no_param_engine.connect() as conn: + assert_raises_message( + tsa.exc.StatementError, + r"\(sqlalchemy.exc.InvalidRequestError\) A value is required " + r"for bind parameter 'the_data_2'\n" + r"\[SQL: SELECT foo.data \nFROM foo \nWHERE " + r"foo.data = \? OR foo.data = \?\]\n" + r"\[SQL parameters hidden due to hide_parameters=True\]", + conn.execute, + select([foo]).where( + or_( + foo.c.data == bindparam("the_data_1"), + foo.c.data == bindparam("the_data_2"), + ) + ), + {"the_data_1": "some data"}, + ) + def test_exception_format_unexpected_parameter(self): # test that if the parameters aren't any known type, we just # run through repr()