Chris McDonough
2011-12-15 954795d2d5c3fafb3b439904a577391cd96b5751
Merge branch 'feature.prequest' into 1.3-branch
2 files added
4 files modified
376 ■■■■■ changed files
CHANGES.txt 10 ●●●●● patch | view | raw | blame | history
docs/narr/commandline.rst 65 ●●●●● patch | view | raw | blame | history
docs/whatsnew-1.3.rst 9 ●●●●● patch | view | raw | blame | history
pyramid/scripts/prequest.py 150 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_scripts/test_prequest.py 141 ●●●●● patch | view | raw | blame | history
setup.py 1 ●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -1,3 +1,13 @@
Next release
============
Features
--------
- Added a ``prequest`` script (along the lines of ``paster request``).  It is
  documented in the "Command-Line Pyramid" chapter in the section entitled
  "Invoking a Request".
1.3a2 (2011-12-14)
==================
docs/narr/commandline.rst
@@ -121,7 +121,8 @@
Once you've installed your program for development using ``setup.py
develop``, you can use an interactive Python shell to execute expressions in
a Python environment exactly like the one that will be used when your
application runs "for real".  To do so, use the ``pshell`` command.
application runs "for real".  To do so, use the ``pshell`` command line
utility.
The argument to ``pshell`` follows the format ``config_file#section_name``
where ``config_file`` is the path to your application's ``.ini`` file and
@@ -311,7 +312,7 @@
.. code-block:: text
   :linenos:
   [chrism@thinko MyProject]$ ../bin/proutes development.ini#MyProject
   [chrism@thinko MyProject]$ ../bin/proutes development.ini
   Name            Pattern                        View
   ----            -------                        ----                     
   home            /                              <function my_view>
@@ -354,7 +355,7 @@
.. code-block:: text
   :linenos:
   [chrism@thinko pyramid]$ ptweens development.ini
   [chrism@thinko pyramid]$ myenv/bin/ptweens development.ini
   "pyramid.tweens" config value NOT set (implicitly ordered tweens used)
   Implicit Tween Chain
@@ -416,6 +417,64 @@
See :ref:`registering_tweens` for more information about tweens.
.. index::
   single: invoking a request
   single: prequest
.. _invoking_a_request:
Invoking a Request
------------------
You can use the ``prequest`` command-line utility to send a request to your
application and see the response body without starting a server.
There are two required arguments to ``prequest``:
- The config file/section: follows the format ``config_file#section_name``
  where ``config_file`` is the path to your application's ``.ini`` file and
  ``section_name`` is the ``app`` section name inside the ``.ini`` file.  The
  ``section_name`` is optional, it defaults to ``main``.  For example:
  ``development.ini``.
- The path: this should be the non-url-quoted path element of the URL to the
  resource you'd like to be rendered on the server.  For example, ``/``.
For example::
   $ bin/prequest development.ini /
This will print the body of the response to the console on which it was
invoked.
Several options are supported by ``prequest``.  These should precede any
config file name or URL.
``prequest`` has a ``-d`` (aka ``--display-headers``) option which prints the
status and headers returned by the server before the output::
   $ bin/prequest -d development.ini /
This will print the status, then the headers, then the body of the response
to the console.
You can add request header values by using the ``--header`` option::
   $ bin/prequest --header=Host=example.com development.ini /
Headers are added to the WSGI environment by converting them to their
CGI/WSGI equivalents (e.g. ``Host=example.com`` will insert the ``HTTP_HOST``
header variable as the value ``example.com``).  Multiple ``--header`` options
can be supplied.  The special header value ``content-type`` sets the
``CONTENT_TYPE`` in the WSGI environment.
By default, ``prequest`` sends a ``GET`` request.  You can change this by
using the ``-m`` (aka ``--method``) option.  ``GET``, ``HEAD``, ``POST`` and
``DELETE`` are currently supported.  When you use ``POST``, the standard
input of the ``prequest`` process is used as the ``POST`` body::
   $ bin/prequest -mPOST development.ini / < somefile
.. _writing_a_script:
Writing a Script
docs/whatsnew-1.3.rst
@@ -81,9 +81,9 @@
internal Pyramid scaffolding; externally distributed scaffolding may allow
for both ``pcreate`` and/or ``paster create``.
Analogues of ``paster pshell``, ``paster pviews`` and ``paster ptweens`` also
exist under the respective console script names ``pshell``, ``pviews``, and
``ptweens``.
Analogues of ``paster pshell``, ``paster pviews``, ``paster request`` and
``paster ptweens`` also exist under the respective console script names
``pshell``, ``pviews``, ``prequest`` and ``ptweens``.
We've replaced use of the Paste ``httpserver`` with the ``wsgiref`` server in
the scaffolds, so once you create a project from a scaffold, its
@@ -296,6 +296,9 @@
- Added a narrative docs chapter named :ref:`scaffolding_chapter`.
- Added a description of the ``prequest`` command-line script at
  :ref:`invoking_a_request`.
Dependency Changes
------------------
pyramid/scripts/prequest.py
New file
@@ -0,0 +1,150 @@
import optparse
import sys
import textwrap
from pyramid.compat import url_quote
from pyramid.request import Request
from pyramid.paster import get_app
def main(argv=sys.argv, quiet=False):
    command = PRequestCommand(argv, quiet)
    return command.run()
class PRequestCommand(object):
    description = """\
    Run a request for the described application.
    This command makes an artifical request to a web application that uses a
    PasteDeploy (.ini) configuration file for the server and application.
    Use "prequest config.ini /path" to request "/path".  Use "prequest
    config.ini /path --method=post < data" to do a POST with the given
    request body.
    If the path is relative (doesn't begin with "/") it is interpreted as
    relative to "/".
    The variable "environ['paste.command_request']" will be set to "True" in
    the request's WSGI environment, so your application can distinguish these
    calls from normal requests.
    Note that you can pass options besides the options listed here; any
    unknown options will be passed to the application in
    "environ['QUERY_STRING']"
    """
    usage = "usage: %prog config_file path_info [args/options]"
    parser = optparse.OptionParser(
        usage=usage,
        description=textwrap.dedent(description)
        )
    parser.add_option(
        '-n', '--app-name',
        dest='app_name',
        metavar= 'NAME',
        help="Load the named application from the config file (default 'main')",
        type="string",
        )
    parser.add_option(
        '--header',
        dest='headers',
        metavar='NAME:VALUE',
        type='string',
        action='append',
        help="Header to add to request (you can use this option multiple times)"
        )
    parser.add_option(
        '-d', '--display-headers',
        dest='display_headers',
        action='store_true',
        help='Display status and headers before the response body'
        )
    parser.add_option(
        '-m', '--method',
        dest='method',
        choices=['GET', 'HEAD', 'POST', 'DELETE'],
        type='choice',
        help='Request method type (GET, POST, DELETE)',
        )
    get_app = staticmethod(get_app)
    stdin = sys.stdin
    def __init__(self, argv, quiet=False):
        self.quiet = quiet
        self.options, self.args = self.parser.parse_args(argv[1:])
    def out(self, msg): # pragma: no cover
        if not self.quiet:
            print(msg)
    def run(self):
        if not len(self.args) >= 2:
            self.out('You must provide at least two arguments')
            return 2
        app_spec = self.args[0]
        path = self.args[1]
        if not path.startswith('/'):
            path = '/' + path
        headers = {}
        if self.options.headers:
            for item in self.options.headers:
                if ':' not in item:
                    self.out(
                        "Bad --header=%s option, value must be in the form "
                        "'name:value'" % item)
                    return 2
                name, value = item.split(':', 1)
                headers[name] = value.strip()
        app = self.get_app(app_spec, self.options.app_name)
        request_method = (self.options.method or 'GET').upper()
        qs = []
        for item in self.args[2:]:
            if '=' in item:
                k, v = item.split('=', 1)
                item = url_quote(k) + '=' + url_quote(v)
            else:
                item = url_quote(item)
            qs.append(item)
        qs = '&'.join(qs)
        environ = {
            'REQUEST_METHOD': request_method,
            'SCRIPT_NAME': '',           # may be empty if app is at the root
            'PATH_INFO': path,             # may be empty if at root of app
            'SERVER_NAME': 'localhost',  # always mandatory
            'SERVER_PORT': '80',         # always mandatory
            'SERVER_PROTOCOL': 'HTTP/1.0',
            'CONTENT_TYPE': 'text/plain',
            'wsgi.run_once': True,
            'wsgi.multithread': False,
            'wsgi.multiprocess': False,
            'wsgi.errors': sys.stderr,
            'wsgi.url_scheme': 'http',
            'wsgi.version': (1, 0),
            'QUERY_STRING': qs,
            'HTTP_ACCEPT': 'text/plain;q=1.0, */*;q=0.1',
            'paste.command_request': True,
            }
        if request_method == 'POST':
            environ['wsgi.input'] = self.stdin
            environ['CONTENT_LENGTH'] = '-1'
        for name, value in headers.items():
            if name.lower() == 'content-type':
                name = 'CONTENT_TYPE'
            else:
                name = 'HTTP_'+name.upper().replace('-', '_')
            environ[name] = value
        request = Request.blank(path, environ=environ)
        response = request.get_response(app)
        if self.options.display_headers:
            self.out(response.status)
            for name, value in response.headerlist:
                self.out('%s: %s' % (name, value))
        self.out(response.ubody)
        return 0
pyramid/tests/test_scripts/test_prequest.py
New file
@@ -0,0 +1,141 @@
import unittest
class TestPRequestCommand(unittest.TestCase):
    def _getTargetClass(self):
        from pyramid.scripts.prequest import PRequestCommand
        return PRequestCommand
    def _makeOne(self, argv):
        cmd = self._getTargetClass()(argv)
        cmd.get_app = self.get_app
        self._out = []
        cmd.out = self.out
        return cmd
    def get_app(self, spec, app_name=None):
        self._spec = spec
        self._app_name = app_name
        def helloworld(environ, start_request):
            self._environ = environ
            self._path_info = environ['PATH_INFO']
            start_request('200 OK', [])
            return [b'abc']
        return helloworld
    def out(self, msg):
        self._out.append(msg)
    def test_command_not_enough_args(self):
        command = self._makeOne([])
        command.run()
        self.assertEqual(self._out, ['You must provide at least two arguments'])
    def test_command_two_args(self):
        command = self._makeOne(['', 'development.ini', '/'])
        command.run()
        self.assertEqual(self._path_info, '/')
        self.assertEqual(self._spec, 'development.ini')
        self.assertEqual(self._app_name, None)
        self.assertEqual(self._out, ['abc'])
    def test_command_path_doesnt_start_with_slash(self):
        command = self._makeOne(['', 'development.ini', 'abc'])
        command.run()
        self.assertEqual(self._path_info, '/abc')
        self.assertEqual(self._spec, 'development.ini')
        self.assertEqual(self._app_name, None)
        self.assertEqual(self._out, ['abc'])
    def test_command_has_bad_config_header(self):
        command = self._makeOne(
            ['', '--header=name','development.ini', '/'])
        command.run()
        self.assertEqual(
            self._out[0],
            ("Bad --header=name option, value must be in the form "
             "'name:value'"))
    def test_command_has_good_header_var(self):
        command = self._makeOne(
            ['', '--header=name:value','development.ini', '/'])
        command.run()
        self.assertEqual(self._environ['HTTP_NAME'], 'value')
        self.assertEqual(self._path_info, '/')
        self.assertEqual(self._spec, 'development.ini')
        self.assertEqual(self._app_name, None)
        self.assertEqual(self._out, ['abc'])
    def test_command_has_content_type_header_var(self):
        command = self._makeOne(
            ['', '--header=content-type:app/foo','development.ini', '/'])
        command.run()
        self.assertEqual(self._environ['CONTENT_TYPE'], 'app/foo')
        self.assertEqual(self._path_info, '/')
        self.assertEqual(self._spec, 'development.ini')
        self.assertEqual(self._app_name, None)
        self.assertEqual(self._out, ['abc'])
    def test_command_has_multiple_header_vars(self):
        command = self._makeOne(
            ['',
             '--header=name:value',
             '--header=name2:value2',
             'development.ini',
             '/'])
        command.run()
        self.assertEqual(self._environ['HTTP_NAME'], 'value')
        self.assertEqual(self._environ['HTTP_NAME2'], 'value2')
        self.assertEqual(self._path_info, '/')
        self.assertEqual(self._spec, 'development.ini')
        self.assertEqual(self._app_name, None)
        self.assertEqual(self._out, ['abc'])
    def test_command_method_get(self):
        command = self._makeOne(['', '--method=GET', 'development.ini', '/'])
        command.run()
        self.assertEqual(self._path_info, '/')
        self.assertEqual(self._spec, 'development.ini')
        self.assertEqual(self._app_name, None)
        self.assertEqual(self._out, ['abc'])
    def test_command_method_post(self):
        from pyramid.compat import NativeIO
        command = self._makeOne(['', '--method=POST', 'development.ini', '/'])
        stdin = NativeIO()
        command.stdin = stdin
        command.run()
        self.assertEqual(self._environ['CONTENT_LENGTH'], '-1')
        self.assertEqual(self._environ['wsgi.input'], stdin)
        self.assertEqual(self._path_info, '/')
        self.assertEqual(self._spec, 'development.ini')
        self.assertEqual(self._app_name, None)
        self.assertEqual(self._out, ['abc'])
    def test_command_extra_args_used_in_query_string(self):
        command = self._makeOne(['', 'development.ini', '/', 'a=1%','b=2','c'])
        command.run()
        self.assertEqual(self._environ['QUERY_STRING'], 'a=1%25&b=2&c')
        self.assertEqual(self._path_info, '/')
        self.assertEqual(self._spec, 'development.ini')
        self.assertEqual(self._app_name, None)
        self.assertEqual(self._out, ['abc'])
    def test_command_display_headers(self):
        command = self._makeOne(
            ['', '--display-headers', 'development.ini', '/'])
        command.run()
        self.assertEqual(self._path_info, '/')
        self.assertEqual(self._spec, 'development.ini')
        self.assertEqual(self._app_name, None)
        self.assertEqual(
            self._out,
            ['200 OK', 'Content-Type: text/html; charset=UTF-8', 'abc'])
class Test_main(unittest.TestCase):
    def _callFUT(self, argv):
        from pyramid.scripts.prequest import main
        return main(argv, True)
    def test_it(self):
        result = self._callFUT(['prequest'])
        self.assertEqual(result, 2)
setup.py
@@ -98,6 +98,7 @@
        proutes = pyramid.scripts.proutes:main
        pviews = pyramid.scripts.pviews:main
        ptweens = pyramid.scripts.ptweens:main
        prequest = pyramid.scripts.prequest:main
        [paste.server_runner]
        wsgiref = pyramid.scripts.pserve:wsgiref_server_runner
        cherrypy = pyramid.scripts.pserve:cherrypy_server_runner