├── 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 |
--------------------------------------------------------------------------------