├── .gitignore ├── MANIFEST.in ├── README.md ├── license.txt ├── setup.cfg ├── setup.py └── threadlocals ├── __init__.py ├── middleware.py ├── tester ├── __init__.py ├── manage.py ├── models.py ├── settings.py ├── testrunner.py ├── tests.py └── urls.py └── threadlocals.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea 3 | MANIFEST 4 | dist 5 | build -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include license.txt 3 | include threadlocals *.py 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Setup/Usage 2 | =========== 3 | Install using `pip install django-threadlocals` 4 | 5 | Add `threadlocals.middleware.ThreadLocalMiddleware` to your `MIDDLEWARE_CLASSES` setting. 6 | Then use it as follows: 7 | 8 | Example usage: 9 | -------------- 10 | ```python 11 | from threadlocals.threadlocals import get_current_request 12 | 13 | request = get_current_request() 14 | ``` 15 | 16 | 17 | Caveat Emptor 18 | ================== 19 | 20 | See [this thread on django-users](https://groups.google.com/forum/?fromgroups=#!topic/django-users/5681nX0YPgQ) for a historical in-depth discussion. This package is production ready, this is the story where it began, a long long time ago: 21 | Having the request in threadlocals is the core piece that allowed us to build a true multi-tenant system on top of django where the site is resolved dynamically based on the current host, 22 | and objects are filtered based on the current host. With the current version of django, this is nearly impossible to do without the request in threadlocals. This was a very significant and advanced undertaking, but we're happy with the results. 23 | 24 | 25 | Tests 26 | ----- 27 | 28 | To run tests: 29 | 30 | `python tester/manage.py test` 31 | 32 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | New BSD style License 2 | -------------------------------------------------------------------- 3 | Copyright (c) 2015 Nutrislice, Inc. 4 | 5 | All rights reserved. 6 | 7 | :Author(s): 8 | - Benjamin Roberts 9 | - Troy Evans 10 | - See below 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions 14 | are met: 15 | 1. Redistributions of source code must retain the above copyright 16 | notice, this list of conditions and the following disclaimer. 17 | 2. Redistributions in binary form must reproduce the above copyright 18 | notice, this list of conditions and the following disclaimer in the 19 | documentation and/or other materials provided with the distribution. 20 | 3. The names of the Author(s) may not be used to 21 | endorse or promote products derived from this software without specific 22 | prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 25 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 26 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 27 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 28 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 29 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | 36 | 37 | _______________________________________ 38 | Derived from django-threaded-multihost 39 | --------------------------------------- 40 | 41 | :Authors of django-threaded-multihost: 42 | - Herbert Poul http://sct.sphene.net 43 | - Bruce Kroeze http://gosatchmo.com 44 | 45 | 46 | Branched from [http://code.djangoproject.com/wiki/CookBookthreadlocalsAndUser CookBookThreadLocalsAndUser] 47 | as modified by [http://sct.sphene.net Sphene Community tools]. 48 | 49 | :django-threaded-multi-host copyright & license: 50 | 51 | New BSD License 52 | =============== 53 | Copyright (c) 2008, Bruce Kroeze http://solidsitesolutions.com 54 | 55 | All rights reserved. 56 | 57 | Redistribution and use in source and binary forms, with or without 58 | modification, are permitted provided that the following conditions are met: 59 | 60 | * Redistributions of source code must retain the above copyright notice, 61 | this list of conditions and the following disclaimer. 62 | * Redistributions in binary form must reproduce the above copyright notice, 63 | this list of conditions and the following disclaimer in the documentation 64 | and/or other materials provided with the distribution. 65 | * Neither the name of SolidSiteSolutions LLC, Zefamily LLC nor the names of its 66 | contributors may be used to endorse or promote products derived from this 67 | software without specific prior written permission. 68 | 69 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 70 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 71 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 72 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 73 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 74 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 75 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 76 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 77 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 78 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 79 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 80 | 81 | 82 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # User: Troy Evans 3 | # Date: 1/24/13 4 | # Time: 8:54 PM 5 | # 6 | # Copyright 2015, Nutrislice Inc. All rights reserved. 7 | 8 | import setuptools 9 | 10 | with open("README.md", "r") as fh: 11 | long_description = fh.read() 12 | 13 | setuptools.setup( 14 | name="django-threadlocals", 15 | packages=setuptools.find_packages(), 16 | version="0.10", 17 | author="Ben Roberts", 18 | author_email="ben@nutrislice.com", 19 | description="Contains utils for storing and retreiving values from threadlocals, and middleware for placing the current Django request in threadlocal storage.", 20 | long_description=long_description, 21 | long_description_content_type="text/markdown", 22 | url='https://github.com/nebstrebor/django-threadlocals', 23 | classifiers=[ 24 | "Programming Language :: Python :: 2", 25 | "License :: OSI Approved :: BSD License", 26 | "Operating System :: OS Independent", 27 | "Framework :: Django", 28 | ], 29 | ) -------------------------------------------------------------------------------- /threadlocals/__init__.py: -------------------------------------------------------------------------------- 1 | # User: Troy Evans 2 | # Date: 1/24/13 3 | # Time: 8:06 PM 4 | # 5 | # Copyright 2012, Nutrislice Inc. 6 | VERSION = '0.10' -------------------------------------------------------------------------------- /threadlocals/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | threadlocals Middleware, provides a better, faster way to get at request and user. 4 | 5 | :Authors: 6 | - Ben Roberts (Nutrislice, Inc.) 7 | - Troy Evans (Nutrislice, Inc.) 8 | - Herbert Poul http://sct.sphene.net 9 | - Bruce Kroeze 10 | 11 | Branched from [http://code.djangoproject.com/wiki/CookBookthreadlocalsAndUser CookBookThreadLocalsAndUser] 12 | as modified by [http://sct.sphene.net Sphene Community tools]. 13 | 14 | (see license.txt) 15 | """ 16 | 17 | from .threadlocals import set_thread_variable, del_thread_variables 18 | try: 19 | from django.utils.deprecation import MiddlewareMixin 20 | except ImportError: 21 | MiddlewareMixin = object 22 | 23 | class ThreadLocalMiddleware(MiddlewareMixin): 24 | """Middleware that puts the request object in thread local storage.""" 25 | 26 | def process_request(self, request): 27 | set_thread_variable('request', request) 28 | # set_current_user(request.user) # not going to store user in TL's for now, since we can get it from the request if we need it, and I read somewhere that accessing reqeust.user can potentially prevent view caching from functioning correctly 29 | 30 | def process_response(self, request, response): 31 | del_thread_variables() 32 | return response 33 | 34 | def process_exception(self, request, exception): 35 | del_thread_variables() 36 | -------------------------------------------------------------------------------- /threadlocals/tester/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrobster/django-threadlocals/6b8a5097eb1f784420abe556b6f29b8f8a814551/threadlocals/tester/__init__.py -------------------------------------------------------------------------------- /threadlocals/tester/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | sys.path.append('..') 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /threadlocals/tester/models.py: -------------------------------------------------------------------------------- 1 | # standard workaround -- empty models.py so that django test runner will run the tests for this app -------------------------------------------------------------------------------- /threadlocals/tester/settings.py: -------------------------------------------------------------------------------- 1 | # Stripped-down Django settings for testrunner 2 | 3 | DEBUG = True 4 | 5 | SECRET_KEY = 'NOT_SO_SECRET' 6 | 7 | MIDDLEWARE = [ 8 | 'django.middleware.common.CommonMiddleware', 9 | 'threadlocals.middleware.ThreadLocalMiddleware' 10 | ] 11 | 12 | TEST_RUNNER = 'testrunner.NoDbTestRunner' 13 | ROOT_URLCONF = 'tester.urls' 14 | INSTALLED_APPS = [ 15 | 'tester', 16 | ] 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /threadlocals/tester/testrunner.py: -------------------------------------------------------------------------------- 1 | # from django.test import DjangoTestSuiteRunner 2 | from django.test.runner import DiscoverRunner 3 | 4 | 5 | # class NoDbTestRunner(DjangoTestSuiteRunner): 6 | class NoDbTestRunner(DiscoverRunner): 7 | """ A test runner to test without database creation """ 8 | 9 | def setup_databases(self, **kwargs): 10 | """ Override the database creation defined in parent class """ 11 | pass 12 | 13 | def teardown_databases(self, old_config, **kwargs): 14 | """ Override the database teardown defined in parent class """ 15 | pass 16 | -------------------------------------------------------------------------------- /threadlocals/tester/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates two different styles of tests (one doctest and one 3 | unittest). These will both pass when you run "manage.py test". 4 | 5 | Replace these with more appropriate tests for your application. 6 | """ 7 | from django.test import RequestFactory, SimpleTestCase 8 | from django.urls import reverse 9 | 10 | from threadlocals.threadlocals import ( 11 | set_thread_variable, get_thread_variable, get_current_request, 12 | # get_current_session 13 | ) 14 | 15 | 16 | class ThreadlocalsTest(SimpleTestCase): 17 | 18 | def setUp(self): 19 | set_thread_variable('request', None) 20 | 21 | def tearDown(self): 22 | set_thread_variable('request', None) 23 | 24 | def test_get_thread_variable_default(self): 25 | gotten = get_thread_variable('unset', 'default value') 26 | self.assertEqual(gotten, 'default value') 27 | 28 | def test_get_set_thread_variable(self): 29 | set_thread_variable('test', {'test': 'test'}) 30 | gotten = get_thread_variable('test') 31 | self.assertEqual(gotten, {'test': 'test'}) 32 | 33 | def test_get_current_request(self): 34 | self.assertEqual(get_current_request(), None) # tests default (None) 35 | request = RequestFactory().get(u'/') 36 | set_thread_variable('request', request) 37 | self.assertEqual(get_current_request(), request) 38 | 39 | def test_get_current_session(self): 40 | # c = self.client 41 | # request = get_current_request() 42 | # request.session = c.session 43 | # self.assertEqual(get_current_session(), c.session) 44 | pass # not testing for now because it might require a database and the function we're testing is dead simple. Feel free to add if its worth it to you. 45 | 46 | def test_get_current_user(self): 47 | pass 48 | 49 | 50 | class ThreadLocalMiddlewareTest(SimpleTestCase): 51 | 52 | def test_process_request(self): 53 | """ 54 | if ThreadLocalMiddleware is enabled in settings, then running the test client 55 | should trigger the middleware and set the request in thread locals 56 | """ 57 | response = self.client.get(''.join([reverse('query'), '?query=test'])) 58 | self.assertEqual(response.content.decode('utf8'), u"{'query': 'test'}") 59 | # No formal way to order tests, so verify request is deleted here. 60 | self.assertEqual(get_current_request(), None) 61 | 62 | def test_process_exception(self): 63 | with self.assertRaises(Exception): 64 | response = self.client.get(reverse('exception')) 65 | self.assertEqual(get_current_request(), None) 66 | -------------------------------------------------------------------------------- /threadlocals/tester/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | from django.http import HttpResponse 7 | from threadlocals.threadlocals import get_current_request 8 | 9 | 10 | def empty_view(request): 11 | return HttpResponse("") 12 | 13 | 14 | def query_view(request): 15 | content = get_current_request().GET.dict() 16 | return HttpResponse(str(content)) 17 | 18 | 19 | def exception_view(request): 20 | raise Exception('Exception in response.') 21 | 22 | 23 | urlpatterns = [ 24 | # Examples: 25 | url(r'^$', empty_view, name='empty'), 26 | url(r'query/', query_view, name='query'), 27 | url(r'exception/', exception_view, name='exception'), 28 | # url(r'^testrunner/', include('testrunner.foo.urls')), 29 | 30 | # Uncomment the admin/doc line below to enable admin documentation: 31 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 32 | 33 | # Uncomment the next line to enable the admin: 34 | # url(r'^admin/', include(admin.site.urls)), 35 | ] 36 | -------------------------------------------------------------------------------- /threadlocals/threadlocals.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | __init__ module for the threadlocals package 4 | 5 | Derived from django-threaded-multihost (see license.txt) 6 | """ 7 | __docformat__ = "restructuredtext" 8 | 9 | import logging 10 | 11 | log = logging.getLogger('threadlocals.middleware') 12 | 13 | from threading import local 14 | 15 | _threadlocals = local() 16 | _threadvariables = set() 17 | 18 | 19 | def set_thread_variable(key, val): 20 | _threadvariables.add(key) 21 | setattr(_threadlocals, key, val) 22 | 23 | 24 | def get_thread_variable(key, default=None): 25 | return getattr(_threadlocals, key, default) 26 | 27 | 28 | def del_thread_variable(key): 29 | if hasattr(_threadlocals, key): 30 | delattr(_threadlocals, key) 31 | 32 | 33 | def del_thread_variables(): 34 | for key in _threadvariables: 35 | del_thread_variable(key) 36 | 37 | 38 | def get_current_request(): 39 | return get_thread_variable('request', None) 40 | 41 | 42 | def get_current_session(): 43 | req = get_current_request() 44 | if req is None: 45 | return None 46 | return req.session 47 | 48 | 49 | def get_current_user(): 50 | user = get_thread_variable('user', None) 51 | if user is None: 52 | req = get_current_request() 53 | if req == None or not hasattr(req, 'user'): 54 | return None 55 | user = req.user 56 | return user 57 | 58 | 59 | def set_current_user(user): 60 | set_thread_variable('user', user) 61 | 62 | 63 | def set_request_variable(key, val, use_threadlocal_if_no_request=True): 64 | request = get_current_request() 65 | if not request: 66 | if not use_threadlocal_if_no_request: 67 | raise RuntimeError( 68 | "Unable to set request variable. No request available in threadlocals. Is ThreadLocalMiddleware installed?") 69 | set_thread_variable(key, val) 70 | else: 71 | try: 72 | var_store = getattr(request, '_variables') 73 | except AttributeError: 74 | setattr(request, '_variables', {}) 75 | var_store = getattr(request, '_variables') 76 | var_store[key] = val 77 | 78 | 79 | def get_request_variable(key, default=None, use_threadlocal_if_no_request=True): 80 | request = get_current_request() 81 | if not request: 82 | if not use_threadlocal_if_no_request: 83 | raise RuntimeError( 84 | "Unable to get request variable. No threadlocal request available. Is ThreadLocalMiddleware installed?") 85 | else: 86 | return get_thread_variable(key, default) 87 | return request._variables.get(key, default) if hasattr(request, '_variables') else default 88 | 89 | --------------------------------------------------------------------------------