diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index 5e565f75d6..a35287e158 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -8,6 +8,7 @@ a string) and returns a tuple in this format: """ import re +from threading import local from django.http import Http404 from django.conf import settings @@ -17,7 +18,6 @@ from django.utils.encoding import iri_to_uri, force_unicode, smart_str from django.utils.functional import memoize from django.utils.importlib import import_module from django.utils.regex_helper import normalize -from django.utils.thread_support import currentThread _resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances. _callable_cache = {} # Maps view and url pattern names to their view functions. @@ -25,10 +25,11 @@ _callable_cache = {} # Maps view and url pattern names to their view functions. # SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for # the current thread (which is the only one we ever access), it is assumed to # be empty. -_prefixes = {} +_prefixes = local() # Overridden URLconfs for each thread are stored here. -_urlconfs = {} +_urlconfs = local() + class ResolverMatch(object): def __init__(self, func, args, kwargs, url_name=None, app_name=None, namespaces=None): @@ -401,7 +402,7 @@ def set_script_prefix(prefix): """ if not prefix.endswith('/'): prefix += '/' - _prefixes[currentThread()] = prefix + _prefixes.value = prefix def get_script_prefix(): """ @@ -409,27 +410,22 @@ def get_script_prefix(): wishes to construct their own URLs manually (although accessing the request instance is normally going to be a lot cleaner). """ - return _prefixes.get(currentThread(), u'/') + return getattr(_prefixes, "value", u'/') def set_urlconf(urlconf_name): """ Sets the URLconf for the current thread (overriding the default one in settings). Set to None to revert back to the default. """ - thread = currentThread() if urlconf_name: - _urlconfs[thread] = urlconf_name + _urlconfs.value = urlconf_name else: - # faster than wrapping in a try/except - if thread in _urlconfs: - del _urlconfs[thread] + if hasattr(_urlconfs, "value"): + del _urlconfs.value def get_urlconf(default=None): """ Returns the root URLconf to use for the current thread if it has been changed from the default one. """ - thread = currentThread() - if thread in _urlconfs: - return _urlconfs[thread] - return default + return getattr(_urlconfs, "value", default) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index d2d5c7d056..3979c9ac8a 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -25,6 +25,11 @@ class BaseDatabaseWrapper(local): self.alias = alias self.use_debug_cursor = None + # Transaction related attributes + self.transaction_state = [] + self.savepoint_state = 0 + self.dirty = None + def __eq__(self, other): return self.alias == other.alias diff --git a/django/db/transaction.py b/django/db/transaction.py index 248cb65cb7..a565da2466 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -25,6 +25,7 @@ except ImportError: from django.conf import settings from django.db import connections, DEFAULT_DB_ALIAS + class TransactionManagementError(Exception): """ This exception is thrown when something bad happens with transaction @@ -32,19 +33,6 @@ class TransactionManagementError(Exception): """ pass -# The states are dictionaries of dictionaries of lists. The key to the outer -# dict is the current thread, and the key to the inner dictionary is the -# connection alias and the list is handled as a stack of values. -state = {} -savepoint_state = {} - -# The dirty flag is set by *_unless_managed functions to denote that the -# code under transaction management has changed things to require a -# database commit. -# This is a dictionary mapping thread to a dictionary mapping connection -# alias to a boolean. -dirty = {} - def enter_transaction_management(managed=True, using=None): """ Enters transaction management for a running thread. It must be balanced with @@ -58,15 +46,14 @@ def enter_transaction_management(managed=True, using=None): if using is None: using = DEFAULT_DB_ALIAS connection = connections[using] - thread_ident = thread.get_ident() - if thread_ident in state and state[thread_ident].get(using): - state[thread_ident][using].append(state[thread_ident][using][-1]) + + if connection.transaction_state: + connection.transaction_state.append(connection.transaction_state[-1]) else: - state.setdefault(thread_ident, {}) - state[thread_ident][using] = [settings.TRANSACTIONS_MANAGED] - if thread_ident not in dirty or using not in dirty[thread_ident]: - dirty.setdefault(thread_ident, {}) - dirty[thread_ident][using] = False + connection.transaction_state.append(settings.TRANSACTIONS_MANAGED) + + if connection.dirty is None: + connection.dirty = False connection._enter_transaction_management(managed) def leave_transaction_management(using=None): @@ -78,16 +65,18 @@ def leave_transaction_management(using=None): if using is None: using = DEFAULT_DB_ALIAS connection = connections[using] + connection._leave_transaction_management(is_managed(using=using)) - thread_ident = thread.get_ident() - if thread_ident in state and state[thread_ident].get(using): - del state[thread_ident][using][-1] + if connection.transaction_state: + del connection.transaction_state[-1] else: - raise TransactionManagementError("This code isn't under transaction management") - if dirty.get(thread_ident, {}).get(using, False): + raise TransactionManagementError("This code isn't under transaction " + "management") + if connection.dirty: rollback(using=using) - raise TransactionManagementError("Transaction managed block ended with pending COMMIT/ROLLBACK") - dirty[thread_ident][using] = False + raise TransactionManagementError("Transaction managed block ended with " + "pending COMMIT/ROLLBACK") + connection.dirty = False def is_dirty(using=None): """ @@ -96,7 +85,9 @@ def is_dirty(using=None): """ if using is None: using = DEFAULT_DB_ALIAS - return dirty.get(thread.get_ident(), {}).get(using, False) + connection = connections[using] + + return connection.dirty def set_dirty(using=None): """ @@ -106,11 +97,13 @@ def set_dirty(using=None): """ if using is None: using = DEFAULT_DB_ALIAS - thread_ident = thread.get_ident() - if thread_ident in dirty and using in dirty[thread_ident]: - dirty[thread_ident][using] = True + connection = connections[using] + + if connection.dirty is not None: + connection.dirty = True else: - raise TransactionManagementError("This code isn't under transaction management") + raise TransactionManagementError("This code isn't under transaction " + "management") def set_clean(using=None): """ @@ -120,9 +113,10 @@ def set_clean(using=None): """ if using is None: using = DEFAULT_DB_ALIAS - thread_ident = thread.get_ident() - if thread_ident in dirty and using in dirty[thread_ident]: - dirty[thread_ident][using] = False + connection = connections[using] + + if connection.dirty is not None: + connection.dirty = False else: raise TransactionManagementError("This code isn't under transaction management") clean_savepoints(using=using) @@ -130,9 +124,8 @@ def set_clean(using=None): def clean_savepoints(using=None): if using is None: using = DEFAULT_DB_ALIAS - thread_ident = thread.get_ident() - if thread_ident in savepoint_state and using in savepoint_state[thread_ident]: - del savepoint_state[thread_ident][using] + connection = connections[using] + connection.savepoint_state = 0 def is_managed(using=None): """ @@ -140,10 +133,9 @@ def is_managed(using=None): """ if using is None: using = DEFAULT_DB_ALIAS - thread_ident = thread.get_ident() - if thread_ident in state and using in state[thread_ident]: - if state[thread_ident][using]: - return state[thread_ident][using][-1] + connection = connections[using] + if connection.transaction_state: + return connection.transaction_state[-1] return settings.TRANSACTIONS_MANAGED def managed(flag=True, using=None): @@ -156,15 +148,16 @@ def managed(flag=True, using=None): if using is None: using = DEFAULT_DB_ALIAS connection = connections[using] - thread_ident = thread.get_ident() - top = state.get(thread_ident, {}).get(using, None) + + top = connection.transaction_state if top: top[-1] = flag if not flag and is_dirty(using=using): connection._commit() set_clean(using=using) else: - raise TransactionManagementError("This code isn't under transaction management") + raise TransactionManagementError("This code isn't under transaction " + "management") def commit_unless_managed(using=None): """ @@ -221,13 +214,11 @@ def savepoint(using=None): using = DEFAULT_DB_ALIAS connection = connections[using] thread_ident = thread.get_ident() - if thread_ident in savepoint_state and using in savepoint_state[thread_ident]: - savepoint_state[thread_ident][using].append(None) - else: - savepoint_state.setdefault(thread_ident, {}) - savepoint_state[thread_ident][using] = [None] + + connection.savepoint_state += 1 + tid = str(thread_ident).replace('-', '') - sid = "s%s_x%d" % (tid, len(savepoint_state[thread_ident][using])) + sid = "s%s_x%d" % (tid, connection.savepoint_state) connection._savepoint(sid) return sid @@ -239,8 +230,8 @@ def savepoint_rollback(sid, using=None): if using is None: using = DEFAULT_DB_ALIAS connection = connections[using] - thread_ident = thread.get_ident() - if thread_ident in savepoint_state and using in savepoint_state[thread_ident]: + + if connection.savepoint_state: connection._savepoint_rollback(sid) def savepoint_commit(sid, using=None): @@ -251,8 +242,8 @@ def savepoint_commit(sid, using=None): if using is None: using = DEFAULT_DB_ALIAS connection = connections[using] - thread_ident = thread.get_ident() - if thread_ident in savepoint_state and using in savepoint_state[thread_ident]: + + if connection.savepoint_state: connection._savepoint_commit(sid) ############## diff --git a/django/utils/thread_support.py b/django/utils/thread_support.py deleted file mode 100644 index 2b97d165ca..0000000000 --- a/django/utils/thread_support.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Code used in a couple of places to work with the current thread's environment. -Current users include i18n and request prefix handling. -""" - -try: - import threading - currentThread = threading.currentThread -except ImportError: - def currentThread(): - return "no threading" - diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index a5c612bfe0..9327ebde0d 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -7,15 +7,16 @@ import sys import warnings import gettext as gettext_module from cStringIO import StringIO +from threading import local from django.utils.importlib import import_module from django.utils.safestring import mark_safe, SafeData -from django.utils.thread_support import currentThread + # Translations are cached in a dictionary for every language+app tuple. # The active translations are stored by threadid to make them thread local. _translations = {} -_active = {} +_active = local() # The default translation is based on the settings file. _default = None @@ -197,16 +198,15 @@ def activate(language): "Please use the 'nb' translation instead.", DeprecationWarning ) - _active[currentThread()] = translation(language) + _active.value = translation(language) def deactivate(): """ Deinstalls the currently active translation object so that further _ calls will resolve against the default translation object, again. """ - global _active - if currentThread() in _active: - del _active[currentThread()] + if hasattr(_active, "value"): + del _active.value def deactivate_all(): """ @@ -214,11 +214,11 @@ def deactivate_all(): useful when we want delayed translations to appear as the original string for some reason. """ - _active[currentThread()] = gettext_module.NullTranslations() + _active.value = gettext_module.NullTranslations() def get_language(): """Returns the currently selected language.""" - t = _active.get(currentThread(), None) + t = getattr(_active, "value", None) if t is not None: try: return t.to_language() @@ -246,8 +246,9 @@ def catalog(): This can be used if you need to modify the catalog or want to access the whole message catalog instead of just translating one string. """ - global _default, _active - t = _active.get(currentThread(), None) + global _default + + t = getattr(_active, "value", None) if t is not None: return t if _default is None: @@ -262,9 +263,10 @@ def do_translate(message, translation_function): translation object to use. If no current translation is activated, the message will be run through the default translation object. """ + global _default + eol_message = message.replace('\r\n', '\n').replace('\r', '\n') - global _default, _active - t = _active.get(currentThread(), None) + t = getattr(_active, "value", None) if t is not None: result = getattr(t, translation_function)(eol_message) else: @@ -300,9 +302,9 @@ def gettext_noop(message): return message def do_ntranslate(singular, plural, number, translation_function): - global _default, _active + global _default - t = _active.get(currentThread(), None) + t = getattr(_active, "value", None) if t is not None: return getattr(t, translation_function)(singular, plural, number) if _default is None: @@ -587,4 +589,3 @@ def get_partial_date_formats(): if month_day_format == 'MONTH_DAY_FORMAT': month_day_format = settings.MONTH_DAY_FORMAT return year_month_format, month_day_format - diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 98c0ed9aaa..deba5d1d03 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -4,10 +4,12 @@ import decimal import os import sys import pickle +from threading import local from django.conf import settings from django.template import Template, Context -from django.utils.formats import get_format, date_format, time_format, localize, localize_input, iter_format_modules +from django.utils.formats import (get_format, date_format, time_format, + localize, localize_input, iter_format_modules) from django.utils.importlib import import_module from django.utils.numberformat import format as nformat from django.utils.safestring import mark_safe, SafeString, SafeUnicode @@ -61,7 +63,7 @@ class TranslationTests(TestCase): self.old_locale_paths = settings.LOCALE_PATHS settings.LOCALE_PATHS += (os.path.join(os.path.dirname(os.path.abspath(__file__)), 'other', 'locale'),) from django.utils.translation import trans_real - trans_real._active = {} + trans_real._active = local() trans_real._translations = {} activate('de') @@ -649,7 +651,7 @@ class ResolutionOrderI18NTests(TestCase): from django.utils.translation import trans_real # Okay, this is brutal, but we have no other choice to fully reset # the translation framework - trans_real._active = {} + trans_real._active = local() trans_real._translations = {} activate('de')