summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2011-12-15 17:29:08 -0500
committerChris McDonough <chrism@plope.com>2011-12-15 17:29:08 -0500
commit954795d2d5c3fafb3b439904a577391cd96b5751 (patch)
tree2ee65323baefebf4fd578083233be3a299a6f510
parent4288cbb755941537a02fff35004309fc13c912dc (diff)
parentc8061ee1d797cb666e1d45e19765ede565d21915 (diff)
downloadpyramid-954795d2d5c3fafb3b439904a577391cd96b5751.tar.gz
pyramid-954795d2d5c3fafb3b439904a577391cd96b5751.tar.bz2
pyramid-954795d2d5c3fafb3b439904a577391cd96b5751.zip
Merge branch 'feature.prequest' into 1.3-branch
-rw-r--r--CHANGES.txt10
-rw-r--r--docs/narr/commandline.rst65
-rw-r--r--docs/whatsnew-1.3.rst9
-rw-r--r--pyramid/scripts/prequest.py150
-rw-r--r--pyramid/tests/test_scripts/test_prequest.py141
-rw-r--r--setup.py1
6 files changed, 370 insertions, 6 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 74f79e5ae..c8a156d2e 100644
--- a/CHANGES.txt
+++ b/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)
==================
diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst
index 66ef46671..b9aa2c8c3 100644
--- a/docs/narr/commandline.rst
+++ b/docs/narr/commandline.rst
@@ -121,7 +121,8 @@ The Interactive Shell
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 @@ For example:
.. 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 @@ configured without any explicit tweens:
.. 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 @@ is used:
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
diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst
index b0afacfe6..a69efd268 100644
--- a/docs/whatsnew-1.3.rst
+++ b/docs/whatsnew-1.3.rst
@@ -81,9 +81,9 @@ under both Python 2 and Python 3. ``pcreate`` is required to be used for
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 @@ Documentation Enhancements
- 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
------------------
diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py
new file mode 100644
index 000000000..6a860c900
--- /dev/null
+++ b/pyramid/scripts/prequest.py
@@ -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
diff --git a/pyramid/tests/test_scripts/test_prequest.py b/pyramid/tests/test_scripts/test_prequest.py
new file mode 100644
index 000000000..34c4b3591
--- /dev/null
+++ b/pyramid/tests/test_scripts/test_prequest.py
@@ -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)
diff --git a/setup.py b/setup.py
index 3d0cfd6fd..b9220285f 100644
--- a/setup.py
+++ b/setup.py
@@ -98,6 +98,7 @@ setup(name='pyramid',
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