Merge branch 'feature.prequest' into 1.3-branch
2 files added
4 files modified
| | |
| | | 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) |
| | | ================== |
| | | |
| | |
| | | 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 |
| | |
| | | .. 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> |
| | |
| | | .. 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 |
| | |
| | | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | |
| | | - 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 |
| | | ------------------ |
| | | |
New file |
| | |
| | | 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 |
New file |
| | |
| | | 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) |
| | |
| | | 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 |