├── VERSION.txt ├── .gitignore ├── tests ├── dummy_django_project │ ├── __init__.py │ ├── settings.py │ └── urls.py ├── requirements.txt ├── test_middleware.py ├── __init__.py ├── test_handler.py └── test_embedded_wsgi.py ├── MANIFEST.in ├── dev-requirements.txt ├── .travis.yml ├── setup.cfg ├── docs ├── api.rst ├── index.rst ├── changelog.rst ├── about.rst ├── routing-args.rst ├── request-objects.rst ├── embedded-apps.rst └── conf.py ├── django_wsgi ├── __init__.py ├── exc.py ├── middleware.py ├── handler.py └── embedded_wsgi.py ├── README.rst ├── LICENSE.txt └── setup.py /VERSION.txt: -------------------------------------------------------------------------------- 1 | 1.0b2 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/build 2 | -------------------------------------------------------------------------------- /tests/dummy_django_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==4.0.1 2 | nose==1.3.7 3 | coveralls==1.1 4 | 5 | Django == 1.7.11 6 | WebOb == 1.5.1 7 | -------------------------------------------------------------------------------- /tests/dummy_django_project/settings.py: -------------------------------------------------------------------------------- 1 | ROOT_URLCONF = "tests.dummy_django_project.urls" 2 | 3 | SECRET_KEY = "secret" 4 | 5 | ALLOWED_HOSTS = ["example.org"] 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.txt 3 | include VERSION.txt 4 | 5 | exclude setup.cfg 6 | exclude MANIFEST.in 7 | recursive-exclude tests * 8 | recursive-exclude docs * 9 | -------------------------------------------------------------------------------- /tests/dummy_django_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns 2 | from django.http import HttpResponse 3 | 4 | 5 | urlpatterns = patterns( 6 | '', 7 | ('^$', lambda request: HttpResponse()), 8 | ) 9 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | 3 | # Tests 4 | -r tests/requirements.txt 5 | nose-pudb == 1.0 6 | 7 | # Documentation 8 | sphinx == 1.3.1 9 | 10 | # Distribution 11 | Wheel == 0.26.0 12 | setuptools == 18.6.1 13 | 14 | # Other tools 15 | ipython 16 | pudb 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | - "pypy" 8 | - "pypy3" 9 | install: pip install . 10 | before_script: 11 | - pip install -r tests/requirements.txt 12 | script: coverage run --source=django_wsgi setup.py test 13 | after_success: 14 | - coveralls 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | release = sdist bdist_wheel upload upload_docs 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | 7 | [build_sphinx] 8 | fresh-env = 1 9 | 10 | [nosetests] 11 | where = tests 12 | verbose = 1 13 | verbosity = 1 14 | detailed-errors = 1 15 | no-path-adjustment = 1 16 | with-coverage = 1 17 | cover-erase = 1 18 | cover-package = django_wsgi 19 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | API documentation for :mod:`django_wsgi` 3 | ======================================== 4 | 5 | .. automodule:: django_wsgi 6 | :synopsis: Enhanced WSGI support for Django 7 | :show-inheritance: 8 | 9 | 10 | .. autoclass:: django_wsgi.handler.DjangoWSGIRequest 11 | :show-inheritance: 12 | 13 | 14 | .. autodata:: django_wsgi.handler.APPLICATION 15 | 16 | 17 | Embedded applications 18 | ===================== 19 | 20 | .. autofunction:: django_wsgi.embedded_wsgi.make_wsgi_view 21 | 22 | .. autofunction:: django_wsgi.embedded_wsgi.call_wsgi_app 23 | 24 | 25 | Exceptions 26 | ========== 27 | 28 | .. automodule:: django_wsgi.exc 29 | :synopsis: Exceptions ever raised by django-wsgi 30 | :members: 31 | :show-inheritance: 32 | 33 | 34 | -------------------------------------------------------------------------------- /django_wsgi/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2010, 2013, 2degrees Limited. 5 | # All Rights Reserved. 6 | # 7 | # This file is part of django-wsgi , 8 | # which is subject to the provisions of the BSD at 9 | # . A copy of the 10 | # license should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" 11 | # AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT 12 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST 13 | # INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. 14 | # 15 | ############################################################################## 16 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-wsgi -- WSGI as first-class citizen in Django 2 | ==================================================== 3 | 4 | **This project is no longer maintained.** 5 | 6 | .. image:: https://travis-ci.org/2degrees/django-wsgi.svg?branch=master 7 | :target: https://travis-ci.org/2degrees/django-wsgi 8 | 9 | .. image:: https://coveralls.io/repos/2degrees/django-wsgi/badge.svg?branch=master&service=github 10 | :target: https://coveralls.io/github/2degrees/django-wsgi?branch=master 11 | 12 | django-wsgi unlocks Django applications so developers can take advantage of the 13 | wealth of existing WSGI software, as the other popular Python frameworks do. It 14 | won't break you existing Django applications because it's 100% compatible with 15 | Django and you can start using the functionality offered by this library 16 | progressively. It should be really easy to get started, particularly for 17 | developers who are familiar with frameworks like Pylons or TurboGears. 18 | -------------------------------------------------------------------------------- /django_wsgi/exc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2010-2015, 2degrees Limited. 5 | # All Rights Reserved. 6 | # 7 | # This file is part of django-wsgi , 8 | # which is subject to the provisions of the BSD at 9 | # . A copy of the 10 | # license should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" 11 | # AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT 12 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST 13 | # INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. 14 | # 15 | ############################################################################## 16 | """ 17 | Exceptions raised by :mod:`django_wsgi.` 18 | 19 | """ 20 | 21 | __all__ = ("DjangoWSGIException", "ApplicationCallError") 22 | 23 | 24 | class DjangoWSGIException(Exception): 25 | """Base class for exceptions raised by :mod:`django_wsgi`.""" 26 | pass 27 | 28 | 29 | class ApplicationCallError(DjangoWSGIException): 30 | """ 31 | Exception raised when an embedded WSGI application was not called properly. 32 | 33 | """ 34 | pass 35 | -------------------------------------------------------------------------------- /django_wsgi/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2010, 2013, 2degrees Limited. 5 | # All Rights Reserved. 6 | # 7 | # This file is part of django-wsgi , 8 | # which is subject to the provisions of the BSD at 9 | # . A copy of the 10 | # license should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" 11 | # AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT 12 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST 13 | # INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. 14 | # 15 | ############################################################################## 16 | """ 17 | WSGI and Django middleware. 18 | 19 | """ 20 | 21 | __all__ = ("RoutingArgsMiddleware", ) 22 | 23 | 24 | class RoutingArgsMiddleware(object): 25 | """ 26 | Django middleware which implements the `wsgiorg.routing_args standard 27 | `_. 28 | 29 | """ 30 | 31 | def process_view(self, request, view_func, view_args, view_kwargs): 32 | request.environ['wsgiorg.routing_args'] = (view_args, view_kwargs.copy()) 33 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :mod:`django_wsgi`: WSGI as first-class citizen in Django applications 2 | ====================================================================== 3 | 4 | :Sponsored by: `2degrees Limited `_. 5 | :Latest release: |release| 6 | 7 | **django-wsgi** allows you to use the advanced/common functionality in WSGI that 8 | Django projects don't get out-of-the-box, by improving the interoperability 9 | between Django and the rest of the WSGI ecosystem. To accomplish this, 10 | this library provides the following: 11 | 12 | - The ability to use the fully-featured ``Request`` class from WebOb along side 13 | Django's :class:`~django.http.HttpRequest`. 14 | - Functions to run WSGI applications inside Django reliably. 15 | - A Django middleware which implements the `wsgiorg.routing_args 16 | `_ standard. 17 | 18 | By improving Django's interoperability, you gain the ability to rapidly 19 | integrate many third party software with Django, or simply use a component 20 | which you think outperforms Django's current implementation. 21 | 22 | Finally, it's worth mentioning that: 23 | 24 | - This library has been used in production systems since 2009. 25 | - This project is comprehensively tested and fully documented. 26 | - It supports Django v1.6+, CPython 2.7, CPython 3.3+, PyPy and PyPy 3. 27 | 28 | 29 | Contents 30 | ======== 31 | 32 | .. toctree:: 33 | :maxdepth: 2 34 | 35 | request-objects 36 | embedded-apps 37 | routing-args 38 | api 39 | about 40 | changelog 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2010, 2degrees Limited. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of 2degrees nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Releases 3 | ======== 4 | 5 | Version 1 Beta 1 (2015-11-30) 6 | ============================= 7 | 8 | This is a backwards-incompatible fork of `twod.wsgi v1.0.1 9 | `_. 10 | 11 | The most significant change is that the configuration-related functionality has 12 | been split away from **django-wsgi**, as it is not strictly to do with improving 13 | Django-WSGI interoperability. This functionality is now available in the project 14 | `django-pastedeploy-settings 15 | `_. 16 | 17 | Additionally, the following changes were made: 18 | 19 | * :class:`twod.wsgi.handler:TwodWSGIRequest` was renamed to 20 | :class:`django_wsgi.handler:DjangoWSGIRequest`, and 21 | :class:`twod.wsgi.exc:TwodWSGIException` was renamed to 22 | :class:`django_wsgi.exc:DjangoWSGIException`. 23 | * :class:`django_wsgi.handler:DjangoWSGIRequest` is no longer subclassing both 24 | Django's and WebOb's requests. Django's request class is the only parent now, 25 | and the WebOb request instance is available as the instance attribute 26 | ``webob`` (i.e., ``request.webob``). 27 | * Introduced :data:`django_wsgi.handler.APPLICATION` to make it possible to 28 | set our handler directly via the ``WSGI_APPLICATION`` setting. 29 | * Removed the class ``TwodResponse``, which supported the setting of custom 30 | HTTP status reasons, since newer versions of Django now support this. 31 | * Removed ability to import directly from the package :mod:`django_wsgi`. 32 | * Seek operations have been restricted to the ``wsgi.input`` of POST and PUT 33 | requests. This fixes a bug with Django Admin in Django 1.2 where a view 34 | gets the POST arguments even if the request is a GET one. 35 | * Added Django 1.6 compatibility. 36 | * Added Python 3 compatibility. 37 | -------------------------------------------------------------------------------- /docs/about.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | About **django-wsgi** 3 | ===================== 4 | 5 | *django-wsgi* has only one goal, which is to bring WSGI support to Django 6 | to the same level as in the other modern Python frameworks. We tried it to 7 | implement these improvements at the core of Django, but unfortunately `that 8 | is not going to happen 9 | `_ 10 | for the time being. 11 | 12 | This is another project brought to you by `2degrees Limited 13 | `_. 14 | 15 | 16 | Getting help 17 | ============ 18 | 19 | Keep in mind that **django-wsgi is a thin integration layer**, so if you have 20 | questions about the 3rd party software mentioned in this documentation, it's 21 | best to use the support channels for the corresponding project. 22 | 23 | For questions about WebOb, WebTest, PasteDeploy, PasteScript and Paste itself, 24 | use the `paste-users mailing list `_. 25 | `Python's Web-SIG `_ is the 26 | right place for queries about WSGI in general. 27 | 28 | For questions directly related to *django-wsgi* or if you're not sure what's 29 | the right place to ask a given question, please use our `2degrees-floss mailing 30 | list `_. 31 | 32 | 33 | Development 34 | =========== 35 | 36 | We'll only implement the features we're going to use, but if there's something 37 | you're missing, we'd be pleased to apply patches for the features you need, as 38 | long as: 39 | 40 | - They are `PEP 8 `_ and preferably 41 | `257 `_ compliant. 42 | - There are unit tests for the new code. 43 | - The new code doesn't break existing functionality. 44 | 45 | Please go to `our development site on GitHub 46 | `_ to get the 47 | `latest code `_, 48 | fork it (and ask us to merge them into ours) and raise 49 | `issues `_. 50 | -------------------------------------------------------------------------------- /docs/routing-args.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Routing arguments support 3 | ========================= 4 | 5 | `routing_args `_ is an 6 | extension to the WSGI standard which normalises the place to put the 7 | arguments found in the URL. This is particularly useful for 3rd party WSGI 8 | libraries and the dispatching components in Web frameworks 9 | are expected to set it, but Django does not: Therefore we created the 10 | :class:`~django_wsgi.RoutingArgsMiddleware` Django middleware. 11 | 12 | If you requested the path ``/blog/posts/hello-world/comments/3``, then the 13 | arguments ``hello-world`` and ``3`` will be available in the request object. 14 | Depending on how you defined your URL patterns, they'll be a dictionary (if you 15 | used named groups) or a tuple (if you didn't set names for the matching groups). 16 | The former are referred to as "named arguments" and the latter as "positional 17 | arguments" in *routing_args* specification. 18 | 19 | :class:`~django_wsgi.RoutingArgsMiddleware` simply puts the arguments found by the 20 | Django URL resolver in the ``request`` object. It's such a simple thing, but 21 | it's key for Django-independent libraries, which may not be run in the 22 | context of a Django middleware nor a Django view. 23 | 24 | 25 | Named arguments 26 | =============== 27 | 28 | The named arguments for the request are available at ``request.urlvars``, an 29 | attribute provided by :doc:`WebOb `. 30 | 31 | So, if you had the following URL pattern:: 32 | 33 | (r'^/blog/posts/(?\w+)/comments/(?\d+)$', post_comment) 34 | 35 | a request may have the following named arguments:: 36 | 37 | >>> request.urlvars 38 | {'post_slug': "hello-world", 'post_comment_id': "3"} 39 | 40 | 41 | Positional arguments 42 | ==================== 43 | 44 | The positional arguments for the request are available at ``request.urlargs``, 45 | another attribute provided by :doc:`WebOb `. 46 | 47 | If you had the following URL pattern:: 48 | 49 | (r'^/blog/posts/(\w+)/comments/(\d+)$', post_comment) 50 | 51 | a request may have the following named arguments:: 52 | 53 | >>> request.urlvars 54 | ("hello-world", "3") 55 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2010-2015, 2degrees Limited. 4 | # All Rights Reserved. 5 | # 6 | # This file is part of django-wsgi , 7 | # which is subject to the provisions of the BSD at 8 | # . A copy of the 9 | # license should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" 10 | # AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT 11 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST 12 | # INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. 13 | # 14 | ############################################################################## 15 | 16 | import os 17 | 18 | from setuptools import setup, find_packages 19 | 20 | 21 | here = os.path.abspath(os.path.dirname(__file__)) 22 | README = open(os.path.join(here, "README.rst")).read() 23 | version = open(os.path.join(here, "VERSION.txt")).readline().rstrip() 24 | 25 | setup( 26 | name="django-wsgi", 27 | version=version, 28 | description="Enhanced WSGI support for Django applications", 29 | long_description=README, 30 | classifiers=[ 31 | "Development Status :: 5 - Production/Stable", 32 | "Environment :: Web Environment", 33 | "Framework :: Django", 34 | "Intended Audience :: Developers", 35 | "License :: OSI Approved :: BSD License", 36 | "Natural Language :: English", 37 | "Operating System :: OS Independent", 38 | "Programming Language :: Python :: 2", 39 | "Topic :: Internet :: WWW/HTTP", 40 | "Topic :: Internet :: WWW/HTTP :: WSGI", 41 | "Topic :: Security", 42 | ], 43 | keywords="django wsgi webob web", 44 | author="2degrees Limited", 45 | author_email="2degrees-floss@googlegroups.com", 46 | url="https://pythonhosted.org/django-wsgi/", 47 | license="BSD (http://dev.2degreesnetwork.com/p/2degrees-license.html)", 48 | packages=find_packages(exclude=["tests"]), 49 | zip_safe=False, 50 | tests_require=["coverage", "nose"], 51 | install_requires=[ 52 | "Django >= 1.1", 53 | "WebOb >= 1.5", 54 | "six==1.10.0", 55 | "setuptools", 56 | ], 57 | test_suite="nose.collector", 58 | ) 59 | -------------------------------------------------------------------------------- /django_wsgi/handler.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2010-2015, 2degrees Limited. 4 | # All Rights Reserved. 5 | # 6 | # This file is part of django-wsgi , 7 | # which is subject to the provisions of the BSD at 8 | # . A copy of the 9 | # license should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" 10 | # AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT 11 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST 12 | # INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. 13 | # 14 | ############################################################################## 15 | """ 16 | Django request/response handling a la WSGI. 17 | 18 | """ 19 | from django.core.handlers.wsgi import WSGIHandler as DjangoWSGIHandler 20 | from django.core.handlers.wsgi import WSGIRequest as DjangoRequest 21 | from webob import Request as WebobRequest 22 | 23 | 24 | __all__ = ("DjangoWSGIRequest", "DjangoApplication") 25 | 26 | 27 | class DjangoWSGIRequest(DjangoRequest): 28 | """ 29 | Django request that makes an alternative WebOb request available as an 30 | instance attribute. 31 | 32 | .. attribute:: webob 33 | 34 | :class:`webob.Request` instance for the WSGI environment behind the 35 | current Django request. 36 | 37 | """ 38 | 39 | def __init__(self, environ): 40 | webob_request = WebobRequest(environ) 41 | webob_request.make_body_seekable() 42 | super(DjangoWSGIRequest, self).__init__(webob_request.environ) 43 | self.webob = webob_request 44 | 45 | def read(self, *args, **kwargs): 46 | # Make environ['wsgi.input'] readable by Django, if WebOb read it 47 | self._stream.stream.seek(0) 48 | 49 | try: 50 | return super(DjangoWSGIRequest, self).read() 51 | finally: 52 | # Make environ['wsgi.input'] readable by WebOb 53 | self._stream.stream.seek(0) 54 | 55 | 56 | class DjangoApplication(DjangoWSGIHandler): 57 | """ 58 | Django request handler which uses our enhanced WSGI request class. 59 | 60 | """ 61 | 62 | request_class = DjangoWSGIRequest 63 | 64 | 65 | APPLICATION = DjangoApplication() 66 | """WSGI application based on :class:`DjangoApplication`.""" 67 | -------------------------------------------------------------------------------- /docs/request-objects.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | WebOb request objects in Django 3 | =============================== 4 | 5 | *django-wsgi* extends Django's WSGI handler 6 | (:class:`django.core.handlers.wsgi.WSGIHandler`) and the 7 | :class:`~django.http.HttpRequest` class, so that our handler can use our 8 | extended request class. 9 | 10 | As mentioned before, what Django calls "handler" is actually a generic WSGI 11 | application that wraps your Django project. We'll stick to "WSGI application" 12 | from now on. 13 | 14 | This extended WSGI application offers a better compatibility with WSGI 15 | middleware and applications thanks to the integration with 16 | :class:`webob.Request`. `WebOb `_ is another 17 | offers pythonic APIs to WSGI requests (the so-called "WSGI environment") and 18 | responses -- much like :class:`~django.http.HttpRequest` and 19 | :class:`~django.http.HttpResponse`, but better: 20 | 21 | - Django copies some values from the environment into its own request object, 22 | on every request, no matter if you are going to use them or not. 23 | - When you edit a WebOb request, no matter what you do, your changes will be 24 | applied in the actual WSGI environment. This is key to interoperability. 25 | - WebOb covers the environment comprehensively, with getters and other methods 26 | to make it easier and more fun to handle. 27 | - Django refuses to read POST requests if the ``CONTENT_LENGTH`` equals ``"-1"`` 28 | (may happen when read by WSGI middleware), while the intended behaviour 29 | is to **force** wrappers to read it. 30 | 31 | Instances of :class:`our request ` make 32 | an the equivalent WebOb request available as an attribute, so that you can use 33 | WebOb's accessors in your code. For example:: 34 | 35 | def my_view(request): 36 | if 'MSIE' in request.webob.user_agent: 37 | response = HttpResponseNotFound() 38 | else: 39 | response = HttpResponse() 40 | return response 41 | 42 | This request class will be used instead of the built-in one when you configure 43 | Django to use our "handler" in your ``settings.py``:: 44 | 45 | WSGI_APPLICATION = 'django_wsgi.handler.APPLICATION' 46 | 47 | See the `documentation for webob.Request 48 | `_ to learn more about 49 | the new features you now have at your disposal. 50 | 51 | 52 | Using the WSGI application directly 53 | ----------------------------------- 54 | 55 | You can import it as you would normally do in Django:: 56 | 57 | from os import environ 58 | environ['DJANGO_SETTINGS_MODULE'] = "yourpackage.settings" 59 | 60 | from django_wsgi.handler import DjangoApplication 61 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2010, 2013, 2degrees Limited. 5 | # All Rights Reserved. 6 | # 7 | # This file is part of django-wsgi , 8 | # which is subject to the provisions of the BSD at 9 | # . A copy of the 10 | # license should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" 11 | # AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT 12 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST 13 | # INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. 14 | # 15 | ############################################################################## 16 | """ 17 | Tests for the Django middleware. 18 | 19 | """ 20 | import os 21 | 22 | from nose.tools import eq_, ok_ 23 | 24 | from django_wsgi.middleware import RoutingArgsMiddleware 25 | 26 | os.environ['DJANGO_SETTINGS_MODULE'] = "tests.fixtures.sampledjango" 27 | 28 | 29 | class TestRoutingArgs(object): 30 | """Tests for the wsgiorg.routing_args middleware.""" 31 | 32 | def setup(self): 33 | self.mw = RoutingArgsMiddleware() 34 | self.request = MockRequest({}) 35 | 36 | def test_arguments_are_stored(self): 37 | """The positional and named arguments should be stored in the environ""" 38 | args = ("foo", "bar") 39 | kwargs = {'arg': "value"} 40 | self.mw.process_view(self.request, MOCK_VIEW, args, kwargs) 41 | ok_("wsgiorg.routing_args" in self.request.environ) 42 | eq_(len(self.request.environ['wsgiorg.routing_args']), 2) 43 | eq_(self.request.environ['wsgiorg.routing_args'][0], args) 44 | eq_(self.request.environ['wsgiorg.routing_args'][1], kwargs) 45 | 46 | def test_named_arguments_are_copied(self): 47 | """ 48 | The named arguments must be copied, not passed by reference. 49 | 50 | Because Django will pass them on without inspecting the arguments for 51 | the view, unlike other frameworks like Pylons. 52 | 53 | """ 54 | kwargs = {'arg': "value"} 55 | self.mw.process_view(self.request, MOCK_VIEW, ("foo", "bar"), kwargs) 56 | self.request.environ['wsgiorg.routing_args'][1]['new_arg'] = "new_value" 57 | # The original dictionary must have not been modified: 58 | eq_(len(kwargs), 1) 59 | ok_("arg" in kwargs) 60 | 61 | def test_no_response(self): 62 | """A response should never be returned.""" 63 | args = ("foo", "bar") 64 | kwargs = {'arg': "value"} 65 | result = self.mw.process_view(self.request, MOCK_VIEW, args, kwargs) 66 | ok_(result is None) 67 | 68 | 69 | #{ Mock objects 70 | 71 | 72 | MOCK_VIEW = lambda request: "response" 73 | 74 | 75 | class MockRequest(object): 76 | """ 77 | Mock Django request class. 78 | 79 | This way we won't need to set the DJANGO_SETTINGS_MODULE. 80 | 81 | """ 82 | 83 | def __init__(self, environ): 84 | self.environ = environ 85 | 86 | 87 | #} 88 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2010, 2013, 2degrees Limited. 5 | # All Rights Reserved. 6 | # 7 | # This file is part of django-wsgi , 8 | # which is subject to the provisions of the BSD at 9 | # . A copy of the 10 | # license should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" 11 | # AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT 12 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST 13 | # INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. 14 | # 15 | ############################################################################## 16 | """ 17 | Test suite for :mod:`django_wsgi`. 18 | 19 | """ 20 | import os 21 | 22 | from six import StringIO 23 | from django import conf 24 | from django.conf import LazySettings 25 | 26 | 27 | _DJANGO_SETTINGS_MODULE = "tests.dummy_django_project.settings" 28 | os.environ['DJANGO_SETTINGS_MODULE'] = _DJANGO_SETTINGS_MODULE 29 | 30 | 31 | class BaseDjangoTestCase(object): 32 | 33 | def setup(self): 34 | self.__reset_settings() 35 | 36 | def teardown(self): 37 | self.__reset_settings() 38 | 39 | @staticmethod 40 | def __reset_settings(): 41 | os.environ['DJANGO_SETTINGS_MODULE'] = _DJANGO_SETTINGS_MODULE 42 | conf.settings = LazySettings() 43 | 44 | 45 | #{ Mock WSGI apps 46 | 47 | 48 | class MockApp(object): 49 | """ 50 | Mock WSGI application. 51 | 52 | """ 53 | 54 | def __init__(self, status, headers): 55 | self.status = status 56 | self.headers = headers 57 | 58 | def __call__(self, environ, start_response): 59 | self.environ = environ 60 | start_response(self.status, self.headers) 61 | return ["body"] 62 | 63 | 64 | class MockGeneratorApp(MockApp): 65 | """ 66 | Mock WSGI application that returns an iterator. 67 | 68 | """ 69 | 70 | def __call__(self, environ, start_response): 71 | self.environ = environ 72 | start_response(self.status, self.headers) 73 | def gen(): 74 | yield "body" 75 | yield " as" 76 | yield " iterable" 77 | return gen() 78 | 79 | 80 | class MockWriteApp(MockApp): 81 | """ 82 | Mock WSGI app which uses the write() function. 83 | 84 | """ 85 | 86 | def __call__(self, environ, start_response): 87 | self.environ = environ 88 | write = start_response(self.status, self.headers) 89 | write("body") 90 | write(" as") 91 | write(" iterable") 92 | return [] 93 | 94 | 95 | class MockClosingApp(MockApp): 96 | """Mock WSGI app whose response contains a close() method.""" 97 | 98 | def __init__(self, *args, **kwargs): 99 | super(MockClosingApp, self).__init__(*args, **kwargs) 100 | self.app_iter = ClosingAppIter() 101 | 102 | def __call__(self, environ, start_response): 103 | body = super(MockClosingApp, self).__call__(environ, start_response) 104 | self.app_iter.extend(body) 105 | return self.app_iter 106 | 107 | 108 | class ClosingAppIter(list): 109 | """Mock response iterable with a close() method.""" 110 | 111 | def __init__(self, *args, **kwargs): 112 | super(ClosingAppIter, self).__init__(*args, **kwargs) 113 | self.closed = False 114 | 115 | def close(self): 116 | self.closed = True 117 | 118 | 119 | def complete_environ(**environ): 120 | """ 121 | Add the missing items in ``environ``. 122 | 123 | """ 124 | full_environ = { 125 | 'REQUEST_METHOD': "GET", 126 | 'SERVER_NAME': "example.org", 127 | 'SERVER_PORT': "80", 128 | 'SERVER_PROTOCOL': "HTTP/1.1", 129 | 'HOST': "example.org", 130 | 'wsgi.input': StringIO(""), 131 | 'wsgi.url_scheme': "http", 132 | } 133 | full_environ.update(environ) 134 | return full_environ 135 | 136 | #} 137 | -------------------------------------------------------------------------------- /tests/test_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2010-2015, 2degrees Limited. 5 | # All Rights Reserved. 6 | # 7 | # This file is part of django-wsgi , 8 | # which is subject to the provisions of the BSD at 9 | # . A copy of the 10 | # license should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" 11 | # AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT 12 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST 13 | # INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. 14 | # 15 | ############################################################################## 16 | """ 17 | Tests for the WSGI request handler. 18 | 19 | """ 20 | from nose.tools import eq_, ok_, assert_is_instance 21 | from six import BytesIO 22 | from six.moves.urllib.parse import urlencode 23 | from webob import Request 24 | 25 | from tests import BaseDjangoTestCase, complete_environ 26 | from django_wsgi.handler import APPLICATION 27 | from django_wsgi.handler import DjangoApplication 28 | from django_wsgi.handler import DjangoWSGIRequest 29 | 30 | 31 | class TestRequest(BaseDjangoTestCase): 32 | 33 | @staticmethod 34 | def test_webob_request(): 35 | """The WebOb request is available as a public attribute.""" 36 | environ = complete_environ() 37 | request = DjangoWSGIRequest(environ) 38 | 39 | ok_(hasattr(request, 'webob')) 40 | assert_is_instance(request.webob, Request) 41 | eq_(request.environ, request.webob.environ) 42 | 43 | @staticmethod 44 | def test_request_body_read_by_django_first(): 45 | """WebOb is able to read the request after Django.""" 46 | request = _make_stub_post_request() 47 | 48 | eq_(2, len(request.POST)) 49 | eq_(request.POST, request.webob.POST) 50 | 51 | @staticmethod 52 | def test_request_body_read_by_webob_first(): 53 | """Django is able to read the request after WebOb.""" 54 | request = _make_stub_post_request() 55 | 56 | eq_(2, len(request.webob.POST)) 57 | eq_(request.webob.POST, request.POST) 58 | 59 | @staticmethod 60 | def test_unseekable_body_read_by_django(): 61 | request = _make_stub_post_request(_UnseekableFile) 62 | eq_(2, len(request.POST)) 63 | 64 | @staticmethod 65 | def test_unseekable_body_read_by_webob(): 66 | request = _make_stub_post_request(_UnseekableFile) 67 | 68 | eq_(2, len(request.webob.POST)) 69 | 70 | 71 | def _make_stub_post_request(wsgi_input_class=BytesIO): 72 | input_ = urlencode({'foo': "bar", 'bar': "foo"}).encode() 73 | input_length = str(len(input_)) 74 | environ = { 75 | 'REQUEST_METHOD': "POST", 76 | 'CONTENT_TYPE': "application/x-www-form-urlencoded", 77 | 'CONTENT_LENGTH': input_length, 78 | 'wsgi.input': wsgi_input_class(input_), 79 | } 80 | environ = complete_environ(**environ) 81 | request = DjangoWSGIRequest(environ) 82 | return request 83 | 84 | 85 | class TestWSGIHandler(BaseDjangoTestCase): 86 | """Tests for :class:`DjangoApplication`.""" 87 | 88 | def setup(self): 89 | super(TestWSGIHandler, self).setup() 90 | self.handler = _TelltaleHandler() 91 | 92 | def test_right_request_class(self): 93 | """The WSGI handler must use the custom request class.""" 94 | environ = complete_environ(REQUEST_METHOD="GET", PATH_INFO="/") 95 | 96 | def start_response(status, response_headers): 97 | pass 98 | 99 | self.handler(environ, start_response) 100 | 101 | ok_(isinstance(self.handler.request, DjangoWSGIRequest)) 102 | 103 | 104 | def test_handler_instance(): 105 | assert_is_instance(APPLICATION, DjangoApplication) 106 | 107 | 108 | class _UnseekableFile(object): 109 | 110 | def __init__(self, text): 111 | super(_UnseekableFile, self).__init__() 112 | self._text = BytesIO(text) 113 | 114 | def read(self, *args, **kwargs): 115 | return self._text.read(*args, **kwargs) 116 | 117 | 118 | class _TelltaleHandler(DjangoApplication): 119 | 120 | def get_response(self, request): 121 | self.request = request 122 | return super(_TelltaleHandler, self).get_response(request) 123 | -------------------------------------------------------------------------------- /django_wsgi/embedded_wsgi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2009-2015, 2degrees Limited. 5 | # All Rights Reserved. 6 | # 7 | # This file is part of django-wsgi , 8 | # which is subject to the provisions of the BSD at 9 | # . A copy of the 10 | # license should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" 11 | # AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT 12 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST 13 | # INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. 14 | # 15 | ############################################################################## 16 | """ 17 | Utilities to use WSGI applications within Django. 18 | 19 | """ 20 | 21 | from django.http import HttpResponse 22 | from six import PY2 23 | from six import text_type 24 | from six.moves.http_cookies import SimpleCookie 25 | 26 | from django_wsgi.exc import ApplicationCallError 27 | 28 | __all__ = ("call_wsgi_app", "make_wsgi_view") 29 | 30 | 31 | def call_wsgi_app(wsgi_app, request, path_info): 32 | """ 33 | Call the ``wsgi_app`` with ``request`` and return its response. 34 | 35 | :param wsgi_app: The WSGI application to be run. 36 | :type wsgi_app: callable 37 | :param request: The Django request. 38 | :type request: :class:`django_wsgi.handler.DjangoWSGIRequest` 39 | :param path_info: The ``PATH_INFO`` to be used by the WSGI application. 40 | :type path: :class:`basestring` 41 | :raises django_wsgi.exc.ApplicationCallError: If ``path_info`` is not the 42 | last portion of the ``PATH_INFO`` in ``request``. 43 | :return: The response from the WSGI application, turned into a Django 44 | response. 45 | :rtype: :class:`django.http.HttpResponse` 46 | 47 | """ 48 | webob_request = request.webob 49 | new_request = webob_request.copy() 50 | 51 | # Moving the portion of the path consumed by the current view, from the 52 | # PATH_INTO to the SCRIPT_NAME: 53 | if not request.path_info.endswith(path_info): 54 | raise ApplicationCallError("Path %s is not the last portion of the " 55 | "PATH_INFO in the original request (%s)" 56 | % (path_info, request.path_info)) 57 | consumed_path = request.path_info[:-len(path_info)] 58 | new_request.path_info = path_info 59 | new_request.script_name = webob_request.script_name + consumed_path 60 | 61 | # If the user has been authenticated in Django, log him in the WSGI app: 62 | if request.user.is_authenticated(): 63 | new_request.remote_user = request.user.username 64 | 65 | # Cleaning the routing_args, if any. The application should have its own 66 | # arguments, without relying on any arguments from a parent application: 67 | if "wsgiorg.routing_args" in request.environ: 68 | del new_request.environ['wsgiorg.routing_args'] 69 | # And the same for the WebOb ad-hoc attributes: 70 | if "webob.adhoc_attrs" in request.environ: 71 | del new_request.environ['webob.adhoc_attrs'] 72 | 73 | # Calling the WSGI application and getting its response: 74 | (status_line, headers, body) = new_request.call_application(wsgi_app) 75 | 76 | status_code_raw = status_line.split(" ", 1)[0] 77 | status_code = int(status_code_raw) 78 | 79 | # Turning its response into a Django response: 80 | cookies = SimpleCookie() 81 | django_response = HttpResponse(body, status=status_code) 82 | for (header, value) in headers: 83 | if header.upper() == "SET-COOKIE": 84 | if PY2 and isinstance(value, text_type): 85 | # It can't be Unicode: 86 | value = value.encode("us-ascii") 87 | cookies.load(value) 88 | else: 89 | django_response[header] = value 90 | 91 | # Setting the cookies from Django: 92 | for (cookie_name, cookie) in cookies.items(): 93 | cookie_attributes = { 94 | 'key': cookie_name, 95 | 'value': cookie.value, 96 | 'expires': cookie['expires'], 97 | 'path': cookie['path'], 98 | 'domain': cookie['domain'], 99 | } 100 | if cookie['max-age']: 101 | # Starting in Django 1.3 it performs arithmetic operations 102 | # with 'Max-Age' 103 | cookie_attributes['max_age'] = int(cookie['max-age']) 104 | 105 | django_response.set_cookie(**cookie_attributes) 106 | return django_response 107 | 108 | 109 | def make_wsgi_view(wsgi_app): 110 | """ 111 | Return a callable which can be used as a Django view powered by the 112 | ``wsgi_app``. 113 | 114 | :param wsgi_app: The WSGI which will run the view. 115 | :return: The view callable. 116 | 117 | """ 118 | 119 | def view(request, path_info): 120 | return call_wsgi_app(wsgi_app, request, path_info) 121 | 122 | return view 123 | -------------------------------------------------------------------------------- /docs/embedded-apps.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Embedding WSGI applications 3 | =========================== 4 | 5 | The other WSGI frameworks support running external applications from within 6 | the current WSGI application and Django does now thanks to *django-wsgi*. This 7 | gives you the ability to serve 3rd party applications on-the-fly, as well as 8 | control the requests they get and the responses they return (in order to 9 | filter them or replace them completely). 10 | 11 | Nearly all the Python Web applications out there have built-in WSGI support, 12 | including well-known projects like `Trac `_ and 13 | `Mercurial `_. So you'll be able to integrate 14 | them nicely into your Django application, to support Single Sign-On or manage 15 | authorisation, for example -- **Everything would be controlled from your own 16 | application**. 17 | 18 | These WSGI applications can also be Web applications written in a programming 19 | language other than Python, since there are WSGI applications which run other 20 | Web applications as a Web server/gateway would do it (e.g., 21 | :class:`paste.cgiapp.CGIApplication` for CGI scripts). 22 | 23 | Regardless of the technology used by the embedded application, you will always 24 | pass a Django request object and receive a Django response object. As a 25 | consequence, you must be using the :doc:`enhanced request objects 26 | `. 27 | 28 | 29 | .. note:: 30 | 31 | Django people distinguish "projects" from "applications". From a WSGI 32 | point of view, those so-called "Django projects" are "WSGI applications" 33 | and the so-called "Django applications" are just a framework-specific thing 34 | with no equivalent. 35 | 36 | To keep things simple while using the right WSGI terminology, 37 | Django-powered WSGI applications are referred to as "Django applications" 38 | within this documentation. 39 | 40 | 41 | Embedding non-Django applications 42 | ================================= 43 | 44 | You can mount WSGI applications directly in your Django URL configuration, or 45 | call them from a view to filter the requests they get and/or the response they 46 | return. 47 | 48 | 49 | Mounting them as Django views 50 | ----------------------------- 51 | 52 | You just need to load an instance for that application and put it in your 53 | ``URLConf``, like this:: 54 | 55 | # urls.py 56 | 57 | from django_wsgi.embedded_wsgi import make_wsgi_view 58 | from cool_wsgi_app import CoolApplication 59 | 60 | urlpatterns = patterns('', 61 | # ... 62 | (r'^cool-application(/.*)$', make_wsgi_view(CoolApplication())), 63 | # ... 64 | ) 65 | 66 | Note the path to be consumed by this application must be captured by the 67 | regular expression. If you want to give it a name, use ``path_info``. 68 | 69 | When called, this application will get the same request your view received. So, 70 | among other things, if the user is logged in in Django, he will be also be 71 | logged in in the inner WSGI application (as long as it takes the ``REMOTE_USER`` 72 | variable into account). 73 | 74 | .. attention:: 75 | The path to be consumed by the embedded WSGI application must be the last 76 | portion of the original ``PATH_INFO``. Your own application can only consume 77 | the beginning of such a path. So you won't be able to do something like:: 78 | 79 | (r'^cool-application(/.*)/foo$', make_wsgi_view(CoolApplication())) 80 | 81 | 82 | Calling them from a Django view 83 | ------------------------------- 84 | 85 | If you need more control over what the inner application receives and/or what it 86 | returns, you'd need to call it yourself from your view by using 87 | :func:`django_wsgi.call_wsgi_app`. 88 | 89 | Say you want to serve an instance of `Trac `_ and 90 | you need to set the path to the Trac environment on a per request basis 91 | (because you're hosting multiple Trac instances). You would create the 92 | following Django view:: 93 | 94 | from django_wsgi.embedded_wsgi import call_wsgi_app 95 | from trac.web.main import dispatch_request as trac 96 | 97 | def run_trac(request, path_info): 98 | request.environ['trac.env_path'] = "path/to/trac/env" 99 | return call_wsgi_app(trac, request, path_info) 100 | 101 | If you then want to make it use your existing login and logout forms, you 102 | can update the view to make it look like this:: 103 | 104 | from django.shortcuts import redirect 105 | from django_wsgi.embedded_wsgi import call_wsgi_app 106 | from trac.web.main import dispatch_request as trac 107 | 108 | def run_trac(request, path_info): 109 | 110 | if path_info.startswith("/login"): 111 | return redirect("/my-login-form") 112 | if path_info.startswith("/logout"): 113 | return redirect("/my-logout-form") 114 | 115 | request.environ['trac.env_path'] = "path/to/trac/env" 116 | return call_wsgi_app(trac, request, path_info) 117 | 118 | And if you're even more ambitious and want to serve Trac instances on-the-fly, 119 | you'd do this:: 120 | 121 | from django.shortcuts import redirect 122 | from django_wsgi.embedded_wsgi import call_wsgi_app 123 | from trac.web.main import dispatch_request as trac 124 | 125 | def run_trac(request, trac_id, path_info): 126 | 127 | if path_info.startswith("/login"): 128 | return redirect("/my-login-form") 129 | if path_info.startswith("/logout"): 130 | return redirect("/my-logout-form") 131 | 132 | request.environ['trac.env_path'] = "/var/trac-instances/%s" % trac_id 133 | return call_wsgi_app(trac, request, path_info) 134 | 135 | 136 | # urls.py 137 | 138 | urlpatterns = patterns('', 139 | # ... 140 | (r'^tracs/(?\w+)(?/.*)$', "yourpackage.views.run_trac"), 141 | # ... 142 | ) 143 | 144 | 145 | Modifying the response 146 | ~~~~~~~~~~~~~~~~~~~~~~ 147 | 148 | As we mentioned above, you can deal with the response given by the WSGI 149 | application, which is available as a :class:`django.http.HttpResponse` instance. 150 | 151 | You can do anything you want with the response before returning it. If, for 152 | example, you wanted to set the ``Server`` header, you could do it like this:: 153 | 154 | from django_wsgi.embedded_wsgi import call_wsgi_app 155 | from somewhere import wsgi_app 156 | 157 | def run_app(request, path_info): 158 | response = call_wsgi_app(wsgi_app, request, path_info) 159 | response['Server'] = "django-wsgi 1.0" 160 | return response 161 | 162 | .. warning:: **Avoid reading the body of a response!** 163 | 164 | The body of some responses may be generators, which are useful when the 165 | response is so big that has to be sent in chunks (e.g., a video). 166 | If you read their body, you would consume it and thus you would also alter 167 | the status of said body. 168 | 169 | If you do need to read it, check the ``Content-Type`` first to make sure 170 | that's what you're looking for. If it really is, and the body is a 171 | generator, make sure to pass on a proper response body. 172 | 173 | Note it's absolutely fine to deal with the response status and headers, 174 | though. 175 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-wsgi documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Jan 21 14:07:59 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import os 15 | 16 | here = os.path.dirname(os.path.abspath(__file__)) 17 | root = os.path.dirname(here) 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.append(os.path.abspath('.')) 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = [] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'django-wsgi' 44 | copyright = u'2010-2015, 2degrees Limited' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '1.0' 52 | # The full version, including alpha/beta/rc tags. 53 | release = open(os.path.join(root, 'VERSION.txt')).readline().rstrip() 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of documents that shouldn't be included in the build. 66 | #unused_docs = [] 67 | 68 | # List of directories, relative to source directory, that shouldn't be searched 69 | # for source files. 70 | exclude_trees = [] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all documents. 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | #add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. Major themes that come with 96 | # Sphinx are currently 'default' and 'sphinxdoc'. 97 | html_theme = 'alabaster' 98 | 99 | html_sidebars = { 100 | '**': ['about.html', 'navigation.html'] 101 | } 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | html_theme_options = { 107 | 'description': 'WSGI as first-class citizen in Django applications', 108 | 'github_user': '2degrees', 109 | 'github_repo': 'django-wsgi', 110 | } 111 | 112 | # Add any paths that contain custom themes here, relative to this directory. 113 | #html_theme_path = [] 114 | 115 | # The name for this set of Sphinx documents. If None, it defaults to 116 | # " v documentation". 117 | #html_title = None 118 | 119 | # A shorter title for the navigation bar. Default is the same as html_title. 120 | #html_short_title = None 121 | 122 | # The name of an image file (relative to this directory) to place at the top 123 | # of the sidebar. 124 | #html_logo = None 125 | 126 | # The name of an image file (within the static path) to use as favicon of the 127 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 128 | # pixels large. 129 | #html_favicon = None 130 | 131 | # Add any paths that contain custom static files (such as style sheets) here, 132 | # relative to this directory. They are copied after the builtin static files, 133 | # so a file named "default.css" will overwrite the builtin "default.css". 134 | html_static_path = [] 135 | 136 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 137 | # using the given strftime format. 138 | #html_last_updated_fmt = '%b %d, %Y' 139 | 140 | # If true, SmartyPants will be used to convert quotes and dashes to 141 | # typographically correct entities. 142 | #html_use_smartypants = True 143 | 144 | # Additional templates that should be rendered to pages, maps page names to 145 | # template names. 146 | #html_additional_pages = {} 147 | 148 | # If false, no module index is generated. 149 | #html_use_modindex = True 150 | 151 | # If false, no index is generated. 152 | #html_use_index = True 153 | 154 | # If true, the index is split into individual pages for each letter. 155 | #html_split_index = False 156 | 157 | # If true, links to the reST sources are added to the pages. 158 | #html_show_sourcelink = True 159 | 160 | # If true, an OpenSearch description file will be output, and all pages will 161 | # contain a tag referring to it. The value of this option must be the 162 | # base URL from which the finished HTML is served. 163 | #html_use_opensearch = '' 164 | 165 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 166 | #html_file_suffix = '' 167 | 168 | # Output file base name for HTML help builder. 169 | htmlhelp_basename = 'djangowsgidoc' 170 | 171 | 172 | # -- Options for LaTeX output -------------------------------------------------- 173 | 174 | # The paper size ('letter' or 'a4'). 175 | #latex_paper_size = 'letter' 176 | 177 | # The font size ('10pt', '11pt' or '12pt'). 178 | #latex_font_size = '10pt' 179 | 180 | # Grouping the document tree into LaTeX files. List of tuples 181 | # (source start file, target name, title, author, documentclass [howto/manual]). 182 | latex_documents = [ 183 | ('index', 'djangowsgi.tex', u'django-django_wsgi Documentation', 184 | u'2degrees Network', 'manual'), 185 | ] 186 | 187 | # The name of an image file (relative to this directory) to place at the top of 188 | # the title page. 189 | #latex_logo = None 190 | 191 | # For "manual" documents, if this is true, then toplevel headings are parts, 192 | # not chapters. 193 | #latex_use_parts = False 194 | 195 | # Additional stuff for the LaTeX preamble. 196 | #latex_preamble = '' 197 | 198 | # Documents to append as an appendix to all manuals. 199 | #latex_appendices = [] 200 | 201 | # If false, no module index is generated. 202 | #latex_use_modindex = True 203 | 204 | 205 | # Example configuration for intersphinx: refer to the Python standard library. 206 | intersphinx_mapping = { 207 | 'http://docs.python.org/': None, 208 | 'http://pythonpaste.org/': None, 209 | 'http://docs.webob.org/en/latest/': None, 210 | 'http://pythonpaste.org/webtest/': None, 211 | 'http://pythonpaste.org/deploy/': None, 212 | 'http://docs.djangoproject.com/en/dev/': "http://docs.djangoproject.com/en/dev/_objects/", 213 | } 214 | -------------------------------------------------------------------------------- /tests/test_embedded_wsgi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ############################################################################## 3 | # 4 | # Copyright (c) 2009-2015, 2degrees Limited. 5 | # All Rights Reserved. 6 | # 7 | # This file is part of django-wsgi , 8 | # which is subject to the provisions of the BSD at 9 | # . A copy of the 10 | # license should accompany this distribution. THIS SOFTWARE IS PROVIDED "AS IS" 11 | # AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT 12 | # NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST 13 | # INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. 14 | # 15 | ############################################################################## 16 | """ 17 | Tests for the use of WSGI applications within Django. 18 | 19 | """ 20 | from nose.tools import eq_, ok_, assert_false, assert_raises 21 | 22 | from django_wsgi.embedded_wsgi import call_wsgi_app, make_wsgi_view 23 | from django_wsgi.handler import DjangoWSGIRequest 24 | from django_wsgi.exc import ApplicationCallError 25 | 26 | from tests import (BaseDjangoTestCase, MockApp, MockClosingApp, MockWriteApp, 27 | MockGeneratorApp, complete_environ) 28 | 29 | 30 | class TestCallWSGIApp(BaseDjangoTestCase): 31 | """ 32 | Tests for call_wsgi_app() 33 | 34 | """ 35 | 36 | def test_original_environ_not_modified(self): 37 | """The original environ must have not been modified.""" 38 | original_environ = \ 39 | complete_environ(SCRIPT_NAME="/blog", PATH_INFO="/admin/models") 40 | expected_environ = original_environ.copy() 41 | request = _make_request(**original_environ) 42 | # Running the app: 43 | app = MockApp("200 OK", []) 44 | call_wsgi_app(app, request, "/models") 45 | eq_(expected_environ.keys(), request.environ.keys()) 46 | for variable_name, expected_variable_value in expected_environ.items(): 47 | if variable_name == 'wsgi.input': 48 | actual_input = request.environ['wsgi.input'] 49 | eq_(0, actual_input.tell()) 50 | eq_(b"", actual_input.read()) 51 | else: 52 | eq_(expected_variable_value, request.environ[variable_name]) 53 | 54 | def test_routing_args_are_removed(self): 55 | """The ``wsgiorg.routing_args`` environment key must be removed.""" 56 | environ = { 57 | 'wsgiorg.routing_args': ((), {}), 58 | 'PATH_INFO': "/admin/models", 59 | } 60 | environ = complete_environ(**environ) 61 | request = _make_request(**environ) 62 | # Running the app: 63 | app = MockApp("200 OK", []) 64 | call_wsgi_app(app, request, "/models") 65 | ok_("wsgiorg.routing_args" not in app.environ) 66 | 67 | def test_webob_adhoc_attrs_are_removed(self): 68 | """WebOb's ad-hoc attributes must be removed.""" 69 | environ = { 70 | 'PATH_INFO': "/admin/models", 71 | 'wsgiorg.routing_args': ((), {}), 72 | 'webob.adhoc_attrs': {'foo': "bar"}, 73 | } 74 | environ = complete_environ(**environ) 75 | request = _make_request(**environ) 76 | # Running the app: 77 | app = MockApp("200 OK", []) 78 | call_wsgi_app(app, request, "/models") 79 | ok_("webob.adhoc_attrs" not in app.environ) 80 | 81 | def test_mount_point(self): 82 | environ = complete_environ(SCRIPT_NAME="/dev", PATH_INFO="/trac/wiki") 83 | request = _make_request(**environ) 84 | # Running the app: 85 | app = MockApp("200 OK", []) 86 | call_wsgi_app(app, request, "/wiki") 87 | eq_(app.environ['SCRIPT_NAME'], "/dev/trac") 88 | eq_(app.environ['PATH_INFO'], "/wiki") 89 | 90 | def test_incorrect_mount_point(self): 91 | """ 92 | WSGI apps are not run when the path left to them is not the last 93 | portion of the PATH_INFO in the original request. 94 | 95 | """ 96 | environ = complete_environ(SCRIPT_NAME="/dev", 97 | PATH_INFO="/trac/wiki") 98 | request = _make_request(**environ) 99 | path_info = "/trac" 100 | # Running the app: 101 | app = MockApp("200 OK", []) 102 | assert_raises(ApplicationCallError, call_wsgi_app, app, request, 103 | path_info) 104 | 105 | def test_headers_are_copied_over(self): 106 | environ = complete_environ(SCRIPT_NAME="/dev", PATH_INFO="/trac/wiki") 107 | request = _make_request(**environ) 108 | headers = [ 109 | ("X-Foo", "bar"), 110 | ("Content-Type", "text/plain"), 111 | ] 112 | # The same headers, but set in the format used by HttpResponse 113 | expected_headers = { 114 | 'x-foo': ("X-Foo", "bar"), 115 | 'content-type': ("Content-Type", "text/plain"), 116 | } 117 | # Running the app: 118 | app = MockApp("200 OK", headers) 119 | django_response = call_wsgi_app(app, request, "/wiki") 120 | eq_(expected_headers, django_response._headers) 121 | 122 | def test_authenticated_user(self): 123 | environ = complete_environ(SCRIPT_NAME="/dev", PATH_INFO="/trac/wiki") 124 | request = _make_request(authenticated=True, **environ) 125 | # Running the app: 126 | app = MockApp("200 OK", []) 127 | call_wsgi_app(app, request, "/wiki") 128 | eq_("foobar", app.environ['REMOTE_USER']) 129 | 130 | def test_cookies_sent(self): 131 | environ = complete_environ(SCRIPT_NAME="/dev", PATH_INFO="/trac/wiki") 132 | request = _make_request(**environ) 133 | headers = [ 134 | ("Set-Cookie", "arg1=val1"), 135 | ("Set-Cookie", 136 | "arg2=val2; expires=Fri,%2031-Dec-2010%2023:59:59%20GMT"), 137 | ("Set-Cookie", "arg3=val3; path=/"), 138 | ("Set-Cookie", "arg4=val4; path=/wiki"), 139 | ("Set-Cookie", "arg5=val5; domain=.example.org"), 140 | ("Set-Cookie", "arg6=val6; max-age=3600"), 141 | ( 142 | "Set-Cookie", 143 | "arg7=val7; expires=Fri,%2031-Dec-2010%2023:59:59%20GMT; " 144 | "max-age=3600; domain=.example.org; path=/wiki", 145 | ), 146 | # Now let's try an Unicode cookie: 147 | ("Set-Cookie", u"arg8=val8; max-age=3600"), 148 | # TODO: The "secure" cookie *attribute* is broken in SimpleCookie. 149 | # See: http://bugs.python.org/issue1028088 150 | #("Set-Cookie", "arg9=val9; secure"), 151 | ] 152 | expected_cookies = { 153 | 'arg1': {'value': "val1"}, 154 | 'arg2': { 155 | 'value': "val2", 156 | 'expires': "Fri,%2031-Dec-2010%2023:59:59%20GMT", 157 | }, 158 | 'arg3': {'value': "val3", 'path': "/"}, 159 | 'arg4': {'value': "val4", 'path': "/wiki"}, 160 | 'arg5': {'value': "val5", 'domain': ".example.org"}, 161 | 'arg6': {'value': "val6", 'max-age': 3600}, 162 | 'arg7': { 163 | 'value': "val7", 164 | 'expires': "Fri,%2031-Dec-2010%2023:59:59%20GMT", 165 | 'path': "/wiki", 166 | 'domain': ".example.org", 167 | 'max-age': 3600, 168 | }, 169 | 'arg8': {'value': "val8", 'max-age': 3600}, 170 | # Why the next item as disabled? Check the `headers` variable above 171 | #'arg9': {'value': "val9", 'secure': True}, 172 | } 173 | # Running the app: 174 | app = MockApp("200 OK", headers) 175 | django_response = call_wsgi_app(app, request, "/wiki") 176 | # Checking the cookies: 177 | eq_(len(expected_cookies), len(django_response.cookies)) 178 | # Finally, let's check each cookie: 179 | for (cookie_set_name, cookie_set) in django_response.cookies.items(): 180 | expected_cookie = expected_cookies[cookie_set_name] 181 | expected_cookie_value = expected_cookie.pop("value") 182 | eq_(expected_cookie_value, cookie_set.value, 183 | 'Cookie "%s" has a wrong value ("%s")' % 184 | (cookie_set_name, cookie_set.value)) 185 | for (attr_key, attr_val) in expected_cookie.items(): 186 | eq_( 187 | cookie_set[attr_key], 188 | attr_val, 189 | 'Attribute "%s" in cookie %r is wrong (%r)' % 190 | (attr_key, cookie_set_name, cookie_set[attr_key]), 191 | ) 192 | 193 | def test_string_as_response(self): 194 | app = MockApp("200 It is OK", [("X-HEADER", "Foo")]) 195 | # Running a request: 196 | environ = complete_environ(SCRIPT_NAME="/dev", PATH_INFO="/blog/posts") 197 | request = _make_request(**environ) 198 | django_response = call_wsgi_app(app, request, "/posts") 199 | 200 | http_response_content = b"body" 201 | eq_(http_response_content, _resolve_response_body(django_response)) 202 | 203 | def test_iterable_as_response(self): 204 | app = MockGeneratorApp("200 It is OK", [("X-HEADER", "Foo")]) 205 | # Running a request: 206 | environ = complete_environ(SCRIPT_NAME="/dev", PATH_INFO="/blog/posts") 207 | request = _make_request(**environ) 208 | django_response = call_wsgi_app(app, request, "/posts") 209 | 210 | http_response_content = b"body as iterable" 211 | eq_(http_response_content, _resolve_response_body(django_response)) 212 | 213 | def test_write_response(self): 214 | app = MockWriteApp("200 It is OK", [("X-HEADER", "Foo")]) 215 | # Running a request: 216 | environ = complete_environ(SCRIPT_NAME="/dev", PATH_INFO="/blog/posts") 217 | request = _make_request(**environ) 218 | django_response = call_wsgi_app(app, request, "/posts") 219 | 220 | http_response_content = b"body as iterable" 221 | eq_(http_response_content, _resolve_response_body(django_response)) 222 | 223 | def test_closure_response(self): 224 | """The .close() method in the response (if any) must be kept.""" 225 | app = MockClosingApp("200 It is OK", []) 226 | # Running a request: 227 | environ = complete_environ(SCRIPT_NAME="/dev", PATH_INFO="/blog/posts") 228 | request = _make_request(**environ) 229 | django_response = call_wsgi_app(app, request, "/posts") 230 | # Checking the .close() call: 231 | assert_false(app.app_iter.closed) 232 | django_response.close() 233 | ok_(app.app_iter.closed) 234 | 235 | 236 | class TestWSGIView(BaseDjangoTestCase): 237 | """ 238 | Tests for make_wsgi_view(). 239 | 240 | """ 241 | 242 | def test_right_path(self): 243 | """ 244 | The WSGI application view must work when called with the right path. 245 | 246 | """ 247 | # Loading a WSGI-powered Django view: 248 | headers = [("X-SALUTATION", "Hey")] 249 | app = MockApp("206 One step at a time", headers) 250 | django_view = make_wsgi_view(app) 251 | # Running a request: 252 | environ = complete_environ(SCRIPT_NAME="/dev", 253 | PATH_INFO="/app1/wsgi-view/foo/bar") 254 | request = _make_request(**environ) 255 | # Checking the response: 256 | django_response = django_view(request, "/foo/bar") 257 | eq_(django_response.status_code, 206) 258 | eq_(("X-SALUTATION", "Hey"), django_response._headers['x-salutation']) 259 | eq_(app.environ['PATH_INFO'], "/foo/bar") 260 | eq_(app.environ['SCRIPT_NAME'], "/dev/app1/wsgi-view") 261 | 262 | def test_not_final_path(self): 263 | """ 264 | The path to be consumed by the WSGI app must be the end of the original 265 | PATH_INFO. 266 | 267 | """ 268 | # Loading a WSGI-powered Django view: 269 | headers = [("X-SALUTATION", "Hey")] 270 | app = MockApp("206 One step at a time", headers) 271 | django_view = make_wsgi_view(app) 272 | # Running a request: 273 | environ = complete_environ(SCRIPT_NAME="/dev", 274 | PATH_INFO="/app1/wsgi-view/foo/bar") 275 | request = _make_request(**environ) 276 | # Checking the response. Note "/foo" is NOT the end of PATH_INFO: 277 | assert_raises(ApplicationCallError, django_view, request, "/foo") 278 | 279 | 280 | #{ Test utilities 281 | 282 | 283 | def _make_request(authenticated=False, **environ): 284 | """ 285 | Make a Django request from the items in the WSGI ``environ``. 286 | 287 | """ 288 | class MockDjangoUser(object): 289 | def __init__(self, authenticated): 290 | self.username = "foobar" 291 | self.authenticated = authenticated 292 | def is_authenticated(self): 293 | return self.authenticated 294 | request = DjangoWSGIRequest(environ) 295 | request.user = MockDjangoUser(authenticated) 296 | return request 297 | 298 | 299 | def _resolve_response_body(response): 300 | body_parts = tuple(response) 301 | body_text = b"".join(body_parts) 302 | return body_text 303 | 304 | 305 | #} 306 | --------------------------------------------------------------------------------