diff options
| author | Chris McDonough <chrism@plope.com> | 2011-12-15 17:29:08 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-12-15 17:29:08 -0500 |
| commit | 954795d2d5c3fafb3b439904a577391cd96b5751 (patch) | |
| tree | 2ee65323baefebf4fd578083233be3a299a6f510 | |
| parent | 4288cbb755941537a02fff35004309fc13c912dc (diff) | |
| parent | c8061ee1d797cb666e1d45e19765ede565d21915 (diff) | |
| download | pyramid-954795d2d5c3fafb3b439904a577391cd96b5751.tar.gz pyramid-954795d2d5c3fafb3b439904a577391cd96b5751.tar.bz2 pyramid-954795d2d5c3fafb3b439904a577391cd96b5751.zip | |
Merge branch 'feature.prequest' into 1.3-branch
| -rw-r--r-- | CHANGES.txt | 10 | ||||
| -rw-r--r-- | docs/narr/commandline.rst | 65 | ||||
| -rw-r--r-- | docs/whatsnew-1.3.rst | 9 | ||||
| -rw-r--r-- | pyramid/scripts/prequest.py | 150 | ||||
| -rw-r--r-- | pyramid/tests/test_scripts/test_prequest.py | 141 | ||||
| -rw-r--r-- | setup.py | 1 |
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) @@ -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 |
