Skip to content

Commit

Permalink
Use _binary prefix for bytes/bytearray parameters
Browse files Browse the repository at this point in the history
- Based on PyMySQL#106 but now disabled by default
- Can be enabled via 'binary_prefix' connection parameter
- Added unit tests to verify behaviour

fix falsely prefixing strings with _binary type identifier (PyMySQL#123)
  • Loading branch information
vtermanis committed Jan 7, 2017
1 parent 2feb5ed commit abc5d93
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 6 deletions.
9 changes: 7 additions & 2 deletions MySQLdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
paramstyle = "format"

from _mysql import *
from MySQLdb.compat import PY2
from MySQLdb.constants import FIELD_TYPE
from MySQLdb.times import Date, Time, Timestamp, \
DateFromTicks, TimeFromTicks, TimestampFromTicks
Expand Down Expand Up @@ -72,8 +73,12 @@ def test_DBAPISet_set_equality_membership():
def test_DBAPISet_set_inequality_membership():
assert FIELD_TYPE.DATE != STRING

def Binary(x):
return bytes(x)
if PY2:
def Binary(x):
return bytearray(x)
else:
def Binary(x):
return bytes(x)

def Connect(*args, **kwargs):
"""Factory function for connections.Connection."""
Expand Down
24 changes: 20 additions & 4 deletions MySQLdb/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ class object, used to create cursors (keyword only)
If True, autocommit is enabled.
If None, autocommit isn't set and server default is used.
:param bool binary_prefix:
If set, the '_binary' prefix will be used for raw byte query
arguments (e.g. Binary). This is disabled by default.
There are a number of undocumented, non-standard methods. See the
documentation for the MySQL C API for some hints on what they do.
"""
Expand Down Expand Up @@ -174,6 +178,7 @@ class object, used to create cursors (keyword only)

use_unicode = kwargs2.pop('use_unicode', use_unicode)
sql_mode = kwargs2.pop('sql_mode', '')
binary_prefix = kwargs2.pop('binary_prefix', False)

client_flag = kwargs.get('client_flag', 0)
client_version = tuple([ numeric_part(n) for n in _mysql.get_client_info().split('.')[:2] ])
Expand All @@ -197,7 +202,7 @@ class object, used to create cursors (keyword only)

db = proxy(self)
def _get_string_literal():
# Note: string_literal() is called for bytes object on Python 3.
# Note: string_literal() is called for bytes object on Python 3 (via bytes_literal)
def string_literal(obj, dummy=None):
return db.string_literal(obj)
return string_literal
Expand All @@ -206,20 +211,26 @@ def _get_unicode_literal():
if PY2:
# unicode_literal is called for only unicode object.
def unicode_literal(u, dummy=None):
return db.literal(u.encode(unicode_literal.charset))
return db.string_literal(u.encode(unicode_literal.charset))
else:
# unicode_literal() is called for arbitrary object.
def unicode_literal(u, dummy=None):
return db.literal(str(u).encode(unicode_literal.charset))
return db.string_literal(str(u).encode(unicode_literal.charset))
return unicode_literal

def _get_bytes_literal():
def bytes_literal(obj, dummy=None):
return b'_binary' + db.string_literal(obj)
return bytes_literal

def _get_string_decoder():
def string_decoder(s):
return s.decode(string_decoder.charset)
return string_decoder

string_literal = _get_string_literal()
self.unicode_literal = unicode_literal = _get_unicode_literal()
bytes_literal = _get_bytes_literal()
self.string_decoder = string_decoder = _get_string_decoder()
if not charset:
charset = self.character_set_name()
Expand All @@ -234,7 +245,12 @@ def string_decoder(s):
self.converter[FIELD_TYPE.VARCHAR].append((None, string_decoder))
self.converter[FIELD_TYPE.BLOB].append((None, string_decoder))

self.encoders[bytes] = string_literal
if binary_prefix:
self.encoders[bytes] = string_literal if PY2 else bytes_literal
self.encoders[bytearray] = bytes_literal
else:
self.encoders[bytes] = string_literal

self.encoders[unicode] = unicode_literal
self._transactional = self.server_capabilities & CLIENT.TRANSACTIONS
if self._transactional:
Expand Down
19 changes: 19 additions & 0 deletions tests/test_MySQLdb_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
# -*- coding: utf-8 -*-
import capabilities
from datetime import timedelta
from contextlib import closing
import unittest
import MySQLdb
from MySQLdb.compat import unicode
from MySQLdb import cursors
from configdb import connection_factory
import warnings


Expand Down Expand Up @@ -180,6 +182,23 @@ def test_warning_propagation(self):
finally:
c.close()

def test_binary_prefix(self):
# verify prefix behaviour when enabled, disabled and for default (disabled)
for binary_prefix in (True, False, None):
kwargs = self.connect_kwargs.copy()
# needs to be set to can guarantee CHARSET response for normal strings
kwargs['charset'] = 'utf8'
if binary_prefix != None:
kwargs['binary_prefix'] = binary_prefix

with closing(connection_factory(**kwargs)) as conn:
with closing(conn.cursor()) as c:
c.execute('SELECT CHARSET(%s)', (MySQLdb.Binary(b'raw bytes'),))
self.assertEqual(c.fetchall()[0][0], 'binary' if binary_prefix else 'utf8')
# normal strings should not get prefix
c.execute('SELECT CHARSET(%s)', ('str',))
self.assertEqual(c.fetchall()[0][0], 'utf8')


if __name__ == '__main__':
if test_MySQLdb.leak_test:
Expand Down

0 comments on commit abc5d93

Please sign in to comment.