From 9bc9b669ca25bb4b6b37cbb2386051fb5e3f18b3 Mon Sep 17 00:00:00 2001 From: Hank Gay Date: Fri, 12 Jan 2018 15:43:56 -0500 Subject: [PATCH 1/5] Version bump. --- django_mysqlpool/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_mysqlpool/__init__.py b/django_mysqlpool/__init__.py index 5d8b54d..f6df0ad 100644 --- a/django_mysqlpool/__init__.py +++ b/django_mysqlpool/__init__.py @@ -12,7 +12,7 @@ from functools import wraps -__version__ = "0.2.1" +__version__ = "0.3.0" def auto_close_db(f): From 6e8c7d9e9412f8b6d908f488e46bd72a20cb741d Mon Sep 17 00:00:00 2001 From: Hank Gay Date: Fri, 12 Jan 2018 15:44:24 -0500 Subject: [PATCH 2/5] Tweak dependency on SQLAlchemy. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 776cd3e..8ba9329 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ REQUIRES = [ - "sqlalchemy >=0.7, <1.0", + "SQLAlchemy >=1.0, <2.0", ] From 398462a7395920c9eac9aaf6958156e9360a9277 Mon Sep 17 00:00:00 2001 From: Hank Gay Date: Fri, 12 Jan 2018 17:00:50 -0500 Subject: [PATCH 3/5] Tweak implementation of HashableDict. The new implementation is a little more robust, and hopefully gets around the issue caused by modern SQLAlchemy releases ('method' object is not iterable). --- django_mysqlpool/backends/mysqlpool/base.py | 23 +++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/django_mysqlpool/backends/mysqlpool/base.py b/django_mysqlpool/backends/mysqlpool/base.py index 66d0f9e..9b13039 100644 --- a/django_mysqlpool/backends/mysqlpool/base.py +++ b/django_mysqlpool/backends/mysqlpool/base.py @@ -64,16 +64,31 @@ class HashableDict(dict): This is not generally useful, but created specifically to hold the ``conv`` parameter that needs to be passed to MySQLdb. + + Thanks to `Alex Martelli`_ for the implementation. + + .. _Alex Martelli: https://stackoverflow.com/a/1151686 """ + def __key(self): + return tuple((k, self[k]) for k in sorted(self)) + def __hash__(self): """Calculate the hash of this ``dict``. - The hash is determined by converting to a sorted tuple of key-value - pairs and hashing that. + The hash is determined by converting to a sorted tuple of + key-value pairs and hashing that. + """ + return hash(self.__key()) + + def __eq__(self, other): + """Determine whether ``other`` is equal to this ``dict``. + + The implementation of this comparison uses the same underlying + mechanism as ``__hash__``, in order to guarantee that ``==`` and + ``.hash`` are in sync. """ - items = [(n, tuple(v)) for n, v in self.items if isiterable(v)] - return hash(tuple(items)) + return self.__key() == other.__key() # Define this here so Django can import it. From 793193973812f4e8ffdb566c77641b405884cb39 Mon Sep 17 00:00:00 2001 From: Hank Gay Date: Tue, 16 Jan 2018 11:11:27 -0500 Subject: [PATCH 4/5] Get rid of some lint complaints about imports. --- django_mysqlpool/backends/mysqlpool/base.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/django_mysqlpool/backends/mysqlpool/base.py b/django_mysqlpool/backends/mysqlpool/base.py index 9b13039..7d09d8d 100644 --- a/django_mysqlpool/backends/mysqlpool/base.py +++ b/django_mysqlpool/backends/mysqlpool/base.py @@ -5,13 +5,8 @@ from __future__ import (absolute_import, print_function, unicode_literals, division) -# Make ``Foo()`` work the same in Python 2 as it does in Python 3. -__metaclass__ = type - - import os - from django.conf import settings from django.db.backends.mysql import base from django.core.exceptions import ImproperlyConfigured @@ -22,6 +17,10 @@ raise ImproperlyConfigured("Error loading SQLAlchemy module: %s" % e) +# Make ``Foo()`` work the same in Python 2 as it does in Python 3. +__metaclass__ = type + + # Global variable to hold the actual connection pool. MYSQLPOOL = None # Default pool type (QueuePool, SingletonThreadPool, AssertionPool, NullPool, From 424036a32bc1f7acd628883817a386c6f1e2573e Mon Sep 17 00:00:00 2001 From: Hank Gay Date: Tue, 16 Jan 2018 11:12:11 -0500 Subject: [PATCH 5/5] Reimplement ``HashableDict``. BEFORE: We were using Alex Martelli's excellent implementation from StackOverflow. Unfortunately, this falls down when attempting to create a ``HashableDict`` with keys that aren't sortable. We had addressed this once before, but in a not-very-clean fashion, and the switch to the new implementation lost it. AFTER: We have a more robust implementation. Rather than attempting to sort the dictionary after the fact, we extend ``collections.OrderedDict`` to ensure that it iterates in the same order; we were only sorting to ensure our comparisons were consistent, and ``OrderedDict`` gives us that without breaking in the case of non-comparable types as keys in the same ``dict``. Further, we are once again turnng the ``dict`` values into ``tuple``s where required (for instance, because ``list`` values will break ``hash``). HOWEVER, in contrast to our earlier ``tuple``-izing implementation, we do *NOT* exclude non-iterable values from our hash and/or equality comparisons. --- django_mysqlpool/backends/mysqlpool/base.py | 30 ++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/django_mysqlpool/backends/mysqlpool/base.py b/django_mysqlpool/backends/mysqlpool/base.py index 7d09d8d..d14d096 100644 --- a/django_mysqlpool/backends/mysqlpool/base.py +++ b/django_mysqlpool/backends/mysqlpool/base.py @@ -5,6 +5,7 @@ from __future__ import (absolute_import, print_function, unicode_literals, division) +import collections import os from django.conf import settings @@ -31,15 +32,6 @@ DEFAULT_POOL_TIMEOUT = 119 -def isiterable(value): - """Determine whether ``value`` is iterable.""" - try: - iter(value) - return True - except TypeError: - return False - - class OldDatabaseProxy(): """Saves a reference to the old connect function. @@ -57,20 +49,34 @@ def connect(self, **kwargs): return self.old_connect(**kwargs) -class HashableDict(dict): +class HashableDict(collections.OrderedDict): """A dictionary that is hashable. This is not generally useful, but created specifically to hold the ``conv`` parameter that needs to be passed to MySQLdb. - Thanks to `Alex Martelli`_ for the implementation. + HT to `Alex Martelli`_ for the idea of using a shared ``__key`` function. .. _Alex Martelli: https://stackoverflow.com/a/1151686 """ + @staticmethod + def _is_iterable(test_me): + """Determine whether ``test_me`` is iterable.""" + try: + iter(test_me) + return True + except TypeError: + return False + + @staticmethod + def _tuplefy_as_needed(val): + return tuple(val) if HashableDict._is_iterable(val) else val + def __key(self): - return tuple((k, self[k]) for k in sorted(self)) + return tuple((k, HashableDict._tuplefy_as_needed(v)) + for k, v in self.items()) def __hash__(self): """Calculate the hash of this ``dict``.