diff options
| author | Chris McDonough <chrism@plope.com> | 2011-12-15 21:36:19 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-12-15 21:36:19 -0500 |
| commit | 63f7a19b58738b8fe5fbe82f362825fa53ddc0d2 (patch) | |
| tree | ae76a8285e901994ba62802b639802d2de2e0b0e | |
| parent | 8e615c4f433c4df9b694c6452f793ed4763434ff (diff) | |
| parent | 61838b76639d6dcf9facd549841a2ed0d07ea012 (diff) | |
| download | pyramid-63f7a19b58738b8fe5fbe82f362825fa53ddc0d2.tar.gz pyramid-63f7a19b58738b8fe5fbe82f362825fa53ddc0d2.tar.bz2 pyramid-63f7a19b58738b8fe5fbe82f362825fa53ddc0d2.zip | |
Merge branch '1.3-branch'
29 files changed, 1041 insertions, 166 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index c979c4dc1..63c84d0f8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,12 +4,42 @@ 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". + +Bug Fixes +--------- + +- Normalized exit values and ``-h`` output for all ``p*`` scripts + (``pviews``, ``proutes``, etc). + +Documentation +------------- + +- Added a section named "Making Your Script into a Console Script" in the + "Command-Line Pyramid" chapter. + +1.3a2 (2011-12-14) +================== + +Features +-------- + - New API: ``pyramid.view.view_defaults``. If you use a class as a view, you can use the new ``view_defaults`` class decorator on the class to provide defaults to the view configuration information used by every ``@view_config`` decorator that decorates a method of that class. It also works against view configurations involving a class made imperatively. +- Added a backwards compatibility knob to ``pcreate`` to emulate ``paster + create`` handling for the ``--list-templates`` option. + +- Changed scaffolding machinery around a bit to make it easier for people who + want to have extension scaffolds that can work across Pyramid 1.0.X, 1.1.X, + 1.2.X and 1.3.X. See the new "Creating Pyramid Scaffolds" chapter in the + narrative documentation for more info. + Documentation ------------- @@ -18,6 +48,18 @@ Documentation - Added API docs for ``view_defaults`` class decorator. +- Added an API docs chapter for ``pyramid.scaffolds``. + +- Added a narrative docs chapter named "Creating Pyramid Scaffolds". + +Backwards Incompatibilities +--------------------------- + +- The ``template_renderer`` method of ``pyramid.scaffolds.PyramidScaffold`` + was renamed to ``render_template``. If you were overriding it, you're a + bad person, because it wasn't an API before now. But we're nice so we're + letting you know. + 1.3a1 (2011-12-09) ================== @@ -22,6 +22,7 @@ Must-Have - Fix deployment recipes in cookbook (discourage proxying without changing server). +- Allow prequest path to have query string variables. Nice-to-Have ------------ diff --git a/docs/api.rst b/docs/api.rst index 979e8f490..d510c0d27 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -26,6 +26,7 @@ documentation is organized alphabetically by module name. api/renderers api/request api/response + api/scaffolds api/scripting api/security api/session diff --git a/docs/api/scaffolds.rst b/docs/api/scaffolds.rst new file mode 100644 index 000000000..827962e19 --- /dev/null +++ b/docs/api/scaffolds.rst @@ -0,0 +1,13 @@ +.. _scaffolds_module: + +:mod:`pyramid.scaffolds` +------------------------ + +.. automodule:: pyramid.scaffolds + + .. autoclass:: pyramid.scaffolds.Template + :members: + + .. autoclass:: pyramid.scaffolds.PyramidTemplate + :members: + diff --git a/docs/conf.py b/docs/conf.py index 9be5db325..5281017e7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -80,7 +80,8 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.4dev' +version = '1.3a2' + # The full version, including alpha/beta/rc tags. release = version @@ -475,7 +476,7 @@ def resig(app, what, name, obj, options, signature, return_annotation): # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. -epub_title = 'The Pyramid Web Application Development Framework, Version 1.4dev' +epub_title = 'The Pyramid Web Application Development Framework, Version 1.3' epub_author = 'Chris McDonough' epub_publisher = 'Agendaless Consulting' epub_copyright = '2008-2011' @@ -492,7 +493,7 @@ epub_scheme = 'ISBN' epub_identifier = '0615445675' # A unique identification for the text. -epub_uid = 'The Pyramid Web Application Development Framework, Version 1.4dev' +epub_uid = 'The Pyramid Web Application Development Framework, Version 1.3' # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. diff --git a/docs/index.rst b/docs/index.rst index df7a422d4..21e587992 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,10 +19,9 @@ When saved to ``helloworld.py``, the above application can be run via: $ easy_install pyramid $ python helloworld.py - serving on 0.0.0.0:8080 view at http://127.0.0.1:8080 -And when you visit ``http://localhost:8080/hello/world`` in a browser, you -will see the text ``Hello, world!``. +When you visit ``http://localhost:8080/hello/world`` in a browser, you will +see the text ``Hello, world!``. See :ref:`firstapp_chapter` for a full explanation of how this application works. Read the :ref:`html_narrative_documentation` to understand how @@ -93,6 +92,7 @@ Narrative documentation in chapter form explaining how to use narr/extending narr/advconfig narr/extconfig + narr/scaffolding narr/threadlocals narr/zca diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index 66ef46671..dc3cff8ac 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 @@ -595,3 +654,234 @@ use the following command: import logging.config logging.config.fileConfig('/path/to/my/development.ini') + +.. index:: + single: console script + +.. _making_a_console_script: + +Making Your Script into a Console Script +---------------------------------------- + +A "console script" is :term:`setuptools` terminology for a script that gets +installed into the ``bin`` directory of a Python :term:`virtualenv` (or +"base" Python environment) when a :term:`distribution` which houses that +script is installed. Because it's installed into the ``bin`` directory of a +virtualenv when the distribution is installed, it's a convenient way to +package and distribute functionality that you can call from the command-line. +It's often more convenient to create a console script than it is to create a +``.py`` script and instruct people to call it with "the right Python +interpreter": because it generates a file that lives in ``bin``, when it's +invoked, it will always use "the right" Python environment, which means it +will always be invoked in an environment where all the libraries it needs +(such as Pyramid) are available. + +In general, you can make your script into a console script by doing the +following: + +- Use an existing distribution (such as one you've already created via + ``pcreate``) or create a new distribution that possesses at least one + package or module. It should, within any module within the distribution, + house a callable (usually a function) that takes no arguments and which + runs any of the code you wish to run. + +- Add a ``[console_scripts]`` section to the ``entry_points`` argument of the + distribution which creates a mapping between a script name and a dotted + name representing the callable you added to your distribution. + +- Run ``setup.py develop``, ``setup.py install``, or ``easy_install`` to get + your distribution reinstalled. When you reinstall your distribution, a + file representing the script that you named in the last step will be in the + ``bin`` directory of the virtualenv in which you installed the + distribution. It will be executable. Invoking it from a terminal will + execute your callable. + +As an example, let's create some code that can be invoked by a console script +that prints the deployment settings of a Pyramid application. To do so, +we'll pretend you have a distribution with a package in it named +``myproject``. Within this package, we'll pretend you've added a +``scripts.py`` module which contains the following code: + +.. code-block:: python + :linenos: + + # myproject.scripts module + + import optparse + import sys + import textwrap + + from pyramid.paster import bootstrap + + def settings_show(): + description = """\ + Print the deployment settings for a Pyramid application. Example: + 'psettings deployment.ini' + """ + usage = "usage: %prog config_uri" + parser = optparse.OptionParser( + usage=usage, + description=textwrap.dedent(description) + ) + parser.add_option( + '-o', '--omit', + dest='omit', + metavar='PREFIX', + type='string', + action='append', + help=("Omit settings which start with PREFIX (you can use this " + "option multiple times)") + ) + + options, args = parser.parse_args(sys.argv[1:]) + if not len(args) >= 1: + print('You must provide at least one argument') + return 2 + config_uri = args[0] + omit = options.omit + if omit is None: + omit = [] + env = bootstrap(config_uri) + settings, closer = env['registry'].settings, env['closer'] + try: + for k, v in settings.items(): + if any([k.startswith(x) for x in omit]): + continue + print('%-40s %-20s' % (k, v)) + finally: + closer() + +This script uses the Python ``optparse`` module to allow us to make sense out +of extra arguments passed to the script. It uses the +:func:`pyramid.paster.bootstrap` function to get information about the the +application defined by a config file, and prints the deployment settings +defined in that config file. + +After adding this script to the package, you'll need to tell your +distribution's ``setup.py`` about its existence. Within your distribution's +top-level directory your ``setup.py`` file will look something like this: + +.. code-block:: python + :linenos: + + import os + + from setuptools import setup, find_packages + + here = os.path.abspath(os.path.dirname(__file__)) + README = open(os.path.join(here, 'README.txt')).read() + CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() + + requires = ['pyramid', 'pyramid_debugtoolbar'] + + setup(name='MyProject', + version='0.0', + description='My project', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pylons", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + tests_require=requires, + test_suite="wiggystatic", + entry_points = """\ + [paste.app_factory] + main = wiggystatic:main + """, + ) + +We're going to change the setup.py file to add an ``[console_scripts]`` +section with in the ``entry_points`` string. Within this section, you should +specify a ``scriptname = dotted.path.to:yourfunction`` line. For example:: + + [console_scripts] + show_settings = myproject.scripts:settings_show + +The ``show_settings`` name will be the name of the script that is installed +into ``bin``. The colon (``:``) between ``myproject.scripts`` and +``settings_show`` above indicates that ``myproject.scripts`` is a Python +module, and ``settings_show`` is the function in that module which contains +the code you'd like to run as the result of someone invoking the +``show_settings`` script from their command line. + +The result will be something like: + +.. code-block:: python + :linenos: + + import os + + from setuptools import setup, find_packages + + here = os.path.abspath(os.path.dirname(__file__)) + README = open(os.path.join(here, 'README.txt')).read() + CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() + + requires = ['pyramid', 'pyramid_debugtoolbar'] + + setup(name='MyProject', + version='0.0', + description='My project', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pylons", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + tests_require=requires, + test_suite="wiggystatic", + entry_points = """\ + [paste.app_factory] + main = wiggystatic:main + [console_scripts] + show_settings = myproject.scripts:settings_show + """, + ) + +Once you've done this, invoking ``$somevirtualenv/bin/python setup.py +develop`` will install a file named ``show_settings`` into the +``$somevirtualenv/bin`` directory with a small bit of Python code that points +to your entry point. It will be executable. Running it without any +arguments will print an error and exit. Running it with a single argument +that is the path of a config file will print the settings. Running it with +an ``--omit=foo`` argument will omit the settings that have keys that start +with ``foo``. Running it with two "omit" options (e.g. ``--omit=foo +--omit=bar``) will omit all settings that have keys that start with either +``foo`` or ``bar``:: + + [chrism@thinko somevenv]$ bin/show_settings development.ini \ + --omit=pyramid \ + --omit=debugtoolbar + debug_routematch False + debug_templates True + reload_templates True + mako.directories [] + debug_notfound False + default_locale_name en + reload_resources False + debug_authorization False + reload_assets False + prevent_http_cache False + +Pyramid's ``pserve``, ``pcreate``, ``pshell``, ``prequest``, ``ptweens`` and +other ``p*`` scripts are implemented as console scripts. When you invoke one +of those, you are using a console script. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 3c6799afb..a1ed6c7ff 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -119,7 +119,7 @@ Here's some sample code that implements a minimal forbidden view: .. code-block:: python :linenos: - from pyramid.views import view_config + from pyramid.view import view_config from pyramid.response import Response def forbidden_view(request): diff --git a/docs/narr/scaffolding.rst b/docs/narr/scaffolding.rst new file mode 100644 index 000000000..3e7b102fd --- /dev/null +++ b/docs/narr/scaffolding.rst @@ -0,0 +1,171 @@ +.. _scaffolding_chapter: + +Creating Pyramid Scaffolds +========================== + +You can extend Pyramid by creating a :term:`scaffold` template. A scaffold +template is useful if you'd like to distribute a customizable configuration +of Pyramid to other users. Once you've created a scaffold, and someone has +installed the distribution that houses the scaffold, they can use the +``pcreate`` script to create a custom version of your scaffold's template. +Pyramid itself uses scaffolds to allow people to bootstrap new projects. For +example, ``pcreate -s alchemy MyStuff`` causes Pyramid to render the +``alchemy`` scaffold template to the ``MyStuff`` directory. + +Basics +------ + +A scaffold template is just a bunch of source files and directories on disk. +A small definition class points at this directory; it is in turn pointed at +by a :term:`setuptools` "entry point" which registers the scaffold so it can +be found by the ``pcreate`` command. + +To create a scaffold template, create a Python :term:`distribution` to house +the scaffold which includes a ``setup.py`` that relies on the ``setuptools`` +package. See `Creating a Package +<http://guide.python-distribute.org/creation.html>`_ for more information +about how to do this. For the sake of example, we'll pretend the +distribution you create is named ``CoolExtension``, and it has a package +directory within it named ``coolextension`` + +Once you've created the distribution put a "scaffolds" directory within your +distribution's package directory, and create a file within that directory +named ``__init__.py`` with something like the following: + +.. code-block:: python + :linenos: + + # CoolExtension/coolextension/scaffolds/__init__.py + + from pyramid.scaffolds import PyramidTemplate + + class CoolExtensionTemplate(PyramidTemplate): + _template_dir = 'coolextension_scaffold' + summary = 'My cool extension' + +Once this is done, within the ``scaffolds`` directory, create a template +directory. Our example used a template directory named +``coolextension_scaffold``. + +As you create files and directories within the template directory, note that: + +- Files which have a name which are suffixed with the value ``_tmpl`` will be + rendered, and replacing any instance of the literal string ``{{var}}`` with + the string value of the variable named ``var`` provided to the scaffold. + +- Files and directories with filenames that contain the string ``+var+`` will + have that string replaced with the value of the ``var`` variable provided + to the scaffold. + +Otherwise, files and directories which live in the template directory will be +copied directly without modification to the ``pcreate`` output location. + +The variables provided by the default ``PyramidTemplate`` include ``project`` +(the project name provided by the user as an argument to ``pcreate``), +``package`` (a lowercasing and normalizing of the project name provided by +the user), ``random_string`` (a long random string), and ``package_logger`` +(the name of the package's logger). + +See Pyramid's "scaffolds" package +(https://github.com/Pylons/pyramid/tree/master/pyramid/scaffolds) for +concrete examples of scaffold directories (``zodb``, ``alchemy``, and +``starter``, for example). + +After you've created the template directory, add the following to the +``entry_points`` value of your distribution's ``setup.py``: + + [pyramid.scaffold] + coolextension=coolextension.scaffolds:CoolExtensionTemplate + +For example:: + + def setup( + ..., + entry_points = """\ + [pyramid.scaffold] + coolextension=coolextension.scaffolds:CoolExtensionTemplate + """ + ) + +Run your distribution's ``setup.py develop`` or ``setup.py install`` +command. After that, you should be able to see your scaffolding template +listed when you run ``pcreate -l``. It will be named ``coolextension`` +because that's the name we gave it in the entry point setup. Running +``pcreate -s coolextension MyStuff`` will then render your scaffold to an +output directory named ``MyStuff``. + +See the module documentation for :mod:`pyramid.scaffolds` for information +about the API of the :class:`pyramid.scaffolds.PyramidScaffold` class and +related classes. You can override methods of this class to get special +behavior. + +Supporting Older Pyramid Versions +--------------------------------- + +Because different versions of Pyramid handled scaffolding differently, if you +want to have extension scaffolds that can work across Pyramid 1.0.X, 1.1.X, +1.2.X and 1.3.X, you'll need to use something like this bit of horror while +defining your scaffold template: + +.. code-block:: python + :linenos: + + try: # pyramid 1.0.X + # "pyramid.paster.paste_script..." doesn't exist past 1.0.X + from pyramid.paster import paste_script_template_renderer + from pyramid.paster import PyramidTemplate + except ImportError: + try: # pyramid 1.1.X, 1.2.X + # trying to import "paste_script_template_renderer" fails on 1.3.X + from pyramid.scaffolds import paste_script_template_renderer + from pyramid.scaffolds import PyramidTemplate + except ImportError: # pyramid >=1.3a2 + paste_script_template_renderer = None + from pyramid.scaffolds import PyramidTemplate + + class CoolExtensionTemplateTemplate(PyramidTemplate): + _template_dir = 'coolextension_scaffold' + summary = 'My cool extension' + template_renderer = staticmethod(paste_script_template_renderer) + +And then in the setup.py of the package that contains your scaffold, define +the template as a target of both ``paste.paster_create_template`` (for +``paster create``) and ``pyramid.scaffold`` (for ``pcreate``):: + + [paste.paster_create_template] + coolextension=coolextension.scaffolds:CoolExtensionTemplate + [pyramid.scaffold] + coolextension=coolextension.scaffolds:CoolExtensionTemplate + +Doing this hideousness will allow your scaffold to work as a ``paster +create`` target (under 1.0, 1.1, or 1.2) or as a ``pcreate`` target (under +1.3). If an invoker tries to run ``paster create`` against a scaffold +defined this way under 1.3, an error is raised instructing them to use +``pcreate`` instead. + +If you want only to support Pyramid 1.3 only, it's much cleaner, and the API +is stable: + +.. code-block:: python + :linenos: + + from pyramid.scaffolds import PyramidTemplate + + class CoolExtensionTemplate(PyramidTemplate): + _template_dir = 'coolextension_scaffold' + summary = 'My cool_extension' + +You only need to specify a ``paste.paster_create_template`` entry point +target in your ``setup.py`` if you want your scaffold to be consumable by +users of Pyramid 1.0, 1.1, or 1.2. To support only 1.3, specifying only the +``pyramid.scaffold`` entry point is good enough. If you want to support both +``paster create`` and ``pcreate`` (meaning you want to support Pyramid 1.2 +and some older version), you'll need to define both. + +Examples +-------- + +Existing third-party distributions which house scaffolding are available via +:term:`PyPI`. The ``pyramid_jqm``, ``pyramid_zcml`` and ``pyramid_jinja2`` +packages house scaffolds. You can install and examine these packages to see +how they work in the quest to develop your own scaffolding. diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 28c161ad0..8dd3c3cdb 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -76,12 +76,14 @@ command:: The ``ini`` configuration file format supported by Pyramid has not changed. As a result, Python 2-only users can install PasteScript manually and use -``paster serve`` and ``paster create`` instead if they like. However, using -``pserve`` and ``pcreate`` will work under both Python 2 and Python 3. +``paster serve`` instead if they like. However, using ``pserve`` will work +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 @@ -101,6 +103,10 @@ actually recommended if you rely on proxying from Apache or Nginx to a ``pserve`` -invoked application. **The wsgiref server is not a production quality server.** See :ref:`alternate_wsgi_server` for more information. +New releases in every older major Pyramid series (1.0.2, 1.1.3, 1.2.5) also +have the ``egg:pyramid#wsgiref`` entry point, so scaffold-writers can depend +on it being there even in older major Pyramid versions. + .. warning:: Previously, paste.httpserver "helped" by converting header values that weren't @@ -286,6 +292,16 @@ Documentation Enhancements - A narrative documentation chapter named :ref:`using_introspection` was added. It describes how to query the introspection system. +- Added an API docs chapter for :mod:`pyramid.scaffolds`. + +- Added a narrative docs chapter named :ref:`scaffolding_chapter`. + +- Added a description of the ``prequest`` command-line script at + :ref:`invoking_a_request`. + +- Added a section to the "Command-Line Pyramid" chapter named + :ref:`making_a_console_script`. + Dependency Changes ------------------ diff --git a/pyramid/scaffolds/__init__.py b/pyramid/scaffolds/__init__.py index 1906e0f51..ab2b3034a 100644 --- a/pyramid/scaffolds/__init__.py +++ b/pyramid/scaffolds/__init__.py @@ -3,10 +3,20 @@ import os from pyramid.compat import native_ -from pyramid.scaffolds.template import Template +from pyramid.scaffolds.template import Template # API class PyramidTemplate(Template): + """ + A class that can be used as a base class for Pyramid scaffolding + templates. + """ def pre(self, command, output_dir, vars): + """ Overrides :meth:`pyramid.scaffold.template.Template.pre`, adding + several variables to the default variables list (including + ``random_string``, and ``package_logger``). It also prevents common + misnamings (such as naming a package "site" or naming a package + logger "root". + """ if vars['package'] == 'site': raise ValueError('Sorry, you may not name your package "site". ' 'The package name "site" has a special meaning in ' @@ -20,6 +30,9 @@ class PyramidTemplate(Template): return Template.pre(self, command, output_dir, vars) def post(self, command, output_dir, vars): # pragma: no cover + """ Overrides :meth:`pyramid.scaffold.template.Template.post`, to + print "Welcome to Pyramid. Sorry for the convenience." after a + successful scaffolding rendering.""" self.out('Welcome to Pyramid. Sorry for the convenience.') return Template.post(self, command, output_dir, vars) diff --git a/pyramid/scaffolds/copydir.py b/pyramid/scaffolds/copydir.py index c99238d33..d55ea165a 100644 --- a/pyramid/scaffolds/copydir.py +++ b/pyramid/scaffolds/copydir.py @@ -5,8 +5,6 @@ import os import sys import pkg_resources -import cgi -import urllib from pyramid.compat import ( input_, diff --git a/pyramid/scaffolds/template.py b/pyramid/scaffolds/template.py index c7797cc1d..39d0e4b3f 100644 --- a/pyramid/scaffolds/template.py +++ b/pyramid/scaffolds/template.py @@ -16,13 +16,22 @@ from pyramid.scaffolds import copydir fsenc = sys.getfilesystemencoding() class Template(object): + """ Inherit from this base class and override methods to use the Pyramid + scaffolding system.""" copydir = copydir # for testing _template_dir = None def __init__(self, name): self.name = name - def template_renderer(self, content, vars, filename=None): + def render_template(self, content, vars, filename=None): + """ Return a bytestring representing a templated file based on the + input (content) and the variable names defined (vars). ``filename`` + is used for exception reporting.""" + # this method must not be named "template_renderer" fbo of extension + # scaffolds that need to work under pyramid 1.2 and 1.3, and which + # need to do "template_renderer = + # staticmethod(paste_script_template_renderer)" content = native_(content, fsenc) try: return bytes_( @@ -32,14 +41,20 @@ class Template(object): raise def module_dir(self): - """Returns the module directory of this template.""" mod = sys.modules[self.__class__.__module__] return os.path.dirname(mod.__file__) def template_dir(self): + """ Return the template directory of the scaffold. By default, it + returns the value of ``os.path.join(self.module_dir(), + self._template_dir)`` (``self.module_dir()`` returns the module in + which your subclass has been defined). If ``self._template_dir`` is + a tuple this method just returns the value instead of trying to + construct a path. If _template_dir is a tuple, it should be a + 2-element tuple: ``(package_name, package_relative_path)``.""" assert self._template_dir is not None, ( "Template %r didn't set _template_dir" % self) - if isinstance( self._template_dir, tuple): + if isinstance(self._template_dir, tuple): return self._template_dir else: return os.path.join(self.module_dir(), self._template_dir) @@ -78,7 +93,7 @@ class Template(object): interactive=command.options.interactive, overwrite=command.options.overwrite, indent=1, - template_renderer=self.template_renderer + template_renderer=self.render_template, ) def makedirs(self, dir): # pragma: no cover @@ -90,6 +105,18 @@ class Template(object): def out(self, msg): # pragma: no cover print(msg) + # hair for exit with usage when paster create is used under 1.3 instead + # of pcreate for extension scaffolds which need to support multiple + # versions of pyramid; the check_vars method is called by pastescript + # only as the result of "paster create"; pyramid doesn't use it. the + # required_templates tuple is required to allow it to get as far as + # calling check_vars. + required_templates = () + def check_vars(self, vars, other): + raise RuntimeError( + 'Under Pyramid 1.3, you should use the "pcreate" command rather ' + 'than "paster create"') + class TypeMapper(dict): def __getitem__(self, item): diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index dacebd6ea..ba4eb0856 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -13,12 +13,13 @@ _bad_chars_re = re.compile('[^a-zA-Z0-9_]') def main(argv=sys.argv, quiet=False): command = PCreateCommand(argv, quiet) - command.run() + return command.run() class PCreateCommand(object): - verbosity = 1 - usage = "usage: %prog [options] distribution_name" - parser = optparse.OptionParser(usage) + verbosity = 1 # required + description = "Render Pyramid scaffolding to an output directory" + usage = "usage: %prog [options] output_directory" + parser = optparse.OptionParser(usage, description=description) parser.add_option('-s', '--scaffold', dest='scaffold_name', action='append', @@ -34,6 +35,11 @@ class PCreateCommand(object): dest='list', action='store_true', help="List all available scaffold names") + parser.add_option('--list-templates', + dest='list', + action='store_true', + help=("A backwards compatibility alias for -l/--list. " + "List all available scaffold names.")) parser.add_option('--simulate', dest='simulate', action='store_true', @@ -57,15 +63,15 @@ class PCreateCommand(object): return self.show_scaffolds() if not self.options.scaffold_name: self.out('You must provide at least one scaffold name') - return + return 2 if not self.args: self.out('You must provide a project name') - return + return 2 available = [x.name for x in self.scaffolds] diff = set(self.options.scaffold_name).difference(available) if diff: self.out('Unavailable scaffolds: %s' % list(diff)) - return + return 2 return self.render_scaffolds() def render_scaffolds(self): @@ -85,7 +91,7 @@ class PCreateCommand(object): for scaffold in self.scaffolds: if scaffold.name == scaffold_name: scaffold.run(self, output_dir, vars) - return True + return 0 def show_scaffolds(self): scaffolds = sorted(self.scaffolds, key=lambda x: x.name) @@ -98,7 +104,7 @@ class PCreateCommand(object): ' '*(max_name-len(scaffold.name)), scaffold.summary)) else: self.out('No scaffolds available') - return True + return 0 def all_scaffolds(self): scaffolds = [] diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py new file mode 100644 index 000000000..073323cd6 --- /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 + --method=POST config.ini /path < 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 arguments besides the options listed here; any + unknown arguments will be passed to the application in + "environ['QUERY_STRING']" + """ + usage = "usage: %prog config_uri 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/scripts/proutes.py b/pyramid/scripts/proutes.py index 570417e95..29ec9e72a 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -1,34 +1,34 @@ import optparse import sys +import textwrap from pyramid.paster import bootstrap def main(argv=sys.argv, quiet=False): command = PRoutesCommand(argv, quiet) - command.run() + return command.run() class PRoutesCommand(object): - """Print all URL dispatch routes used by a Pyramid application in the + description = """\ + Print all URL dispatch routes used by a Pyramid application in the order in which they are evaluated. Each route includes the name of the route, the pattern of the route, and the view callable which will be invoked when the route is matched. - This command accepts one positional argument: - - ``config_uri`` -- specifies the PasteDeploy config file to use for the - interactive shell. The format is ``inifile#name``. If the name is left - off, ``main`` will be assumed. - - Example:: - - $ proutes myapp.ini#main + This command accepts one positional argument named "config_uri". It + specifies the PasteDeploy config file to use for the interactive + shell. The format is "inifile#name". If the name is left off, "main" + will be assumed. Example: "proutes myapp.ini". """ bootstrap = (bootstrap,) - summary = "Print all URL dispatch routes related to a Pyramid application" stdout = sys.stdout + usage = '%prog config_uri' - parser = optparse.OptionParser() + parser = optparse.OptionParser( + usage, + description=textwrap.dedent(description) + ) def __init__(self, argv, quiet=False): self.options, self.args = self.parser.parse_args(argv[1:]) @@ -46,7 +46,7 @@ class PRoutesCommand(object): def run(self, quiet=False): if not self.args: self.out('requires a config file argument') - return + return 2 from pyramid.interfaces import IRouteRequest from pyramid.interfaces import IViewClassifier from pyramid.interfaces import IView @@ -59,7 +59,7 @@ class PRoutesCommand(object): routes = mapper.get_routes() fmt = '%-15s %-30s %-25s' if not routes: - return + return 0 self.out(fmt % ('Name', 'Pattern', 'View')) self.out( fmt % ('-'*len('Name'), '-'*len('Pattern'), '-'*len('View'))) @@ -77,4 +77,5 @@ class PRoutesCommand(object): (IViewClassifier, request_iface, Interface), IView, name='', default=None) self.out(fmt % (route.name, pattern, view_callable)) + return 0 diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index a0a3a8a70..a73cbde3a 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -16,6 +16,7 @@ import os import re import subprocess import sys +import textwrap import threading import time import traceback @@ -28,17 +29,14 @@ MAXFD = 1024 def main(argv=sys.argv, quiet=False): command = PServeCommand(argv, quiet=quiet) - command.run() + return command.run() class DaemonizeException(Exception): pass class PServeCommand(object): - usage = 'CONFIG_FILE [start|stop|restart|status] [var=value]' - takes_config_file = 1 - summary = ("Serve the application described in CONFIG_FILE or control " - "daemon status"), + usage = '%prog config_uri [start|stop|restart|status] [var=value]' description = """\ This command serves a web application that uses a PasteDeploy configuration file for the server and application. @@ -51,7 +49,10 @@ class PServeCommand(object): """ verbose = 1 - parser = optparse.OptionParser() + parser = optparse.OptionParser( + usage, + description=textwrap.dedent(description) + ) parser.add_option( '-n', '--app-name', dest='app_name', @@ -158,7 +159,7 @@ class PServeCommand(object): if not self.args: self.out('You must give a config file') - return + return 2 app_spec = self.args[0] if (len(self.args) > 1 and self.args[1] in self.possible_subcommands): @@ -181,7 +182,7 @@ class PServeCommand(object): if cmd not in (None, 'start', 'stop', 'restart', 'status'): self.out( 'Error: must give start|stop|restart (not %s)' % cmd) - return + return 2 if cmd == 'status' or self.options.show_status: return self.show_status() @@ -244,7 +245,7 @@ class PServeCommand(object): except DaemonizeException as ex: if self.verbose > 0: self.out(str(ex)) - return + return 2 if (self.options.monitor_restart and not os.environ.get(self._monitor_environ_key)): diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index dfac9dbce..7a21eaf98 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -1,6 +1,7 @@ from code import interact import optparse import sys +import textwrap from pyramid.compat import configparser from pyramid.util import DottedNameResolver @@ -10,32 +11,29 @@ from pyramid.paster import setup_logging def main(argv=sys.argv, quiet=False): command = PShellCommand(argv, quiet) - command.run() + return command.run() class PShellCommand(object): - """Open an interactive shell with a :app:`Pyramid` app loaded. - - This command accepts one positional argument: - - ``config_uri`` -- specifies the PasteDeploy config file to use for the - interactive shell. The format is ``inifile#name``. If the name is left - off, ``main`` will be assumed. - - Example:: - - $ pshell myapp.ini#main - - .. note:: If you do not point the loader directly at the section of the - ini file containing your :app:`Pyramid` application, the - command will attempt to find the app for you. If you are - loading a pipeline that contains more than one :app:`Pyramid` - application within it, the loader will use the last one. - + usage = '%prog config_uri' + description = """\ + Open an interactive shell with a Pyramid app loaded. This command + accepts one positional argument named "config_uri" which specifies the + PasteDeploy config file to use for the interactive shell. The format is + "inifile#name". If the name is left off, the Pyramid default application + will be assumed. Example: "pshell myapp.ini#main" + + If you do not point the loader directly at the section of the ini file + containing your Pyramid application, the command will attempt to + find the app for you. If you are loading a pipeline that contains more + than one Pyramid application within it, the loader will use the + last one. """ bootstrap = (bootstrap,) # for testing - summary = "Open an interactive shell with a Pyramid application loaded" - parser = optparse.OptionParser() + parser = optparse.OptionParser( + usage, + description=textwrap.dedent(description) + ) parser.add_option('-p', '--python-shell', action='store', type='string', dest='python_shell', default='', help='ipython | bpython | python') @@ -82,7 +80,7 @@ class PShellCommand(object): def run(self, shell=None): if not self.args: self.out('Requires a config file argument') - return + return 2 config_uri = self.args[0] config_file = config_uri.split('#', 1)[0] setup_logging(config_file) diff --git a/pyramid/scripts/ptweens.py b/pyramid/scripts/ptweens.py index 5bc0c7fbe..d3e17db58 100644 --- a/pyramid/scripts/ptweens.py +++ b/pyramid/scripts/ptweens.py @@ -1,5 +1,6 @@ import optparse import sys +import textwrap from pyramid.interfaces import ITweens @@ -9,31 +10,29 @@ from pyramid.paster import bootstrap def main(argv=sys.argv, quiet=False): command = PTweensCommand(argv, quiet) - command.run() + return command.run() class PTweensCommand(object): - """Print all implicit and explicit :term:`tween` objects used by a - Pyramid application. The handler output includes whether the system is - using an explicit tweens ordering (will be true when the - ``pyramid.tweens`` setting is used) or an implicit tweens ordering (will - be true when the ``pyramid.tweens`` setting is *not* used). - - This command accepts one positional argument: - - ``config_uri`` -- specifies the PasteDeploy config file to use for the - interactive shell. The format is ``inifile#name``. If the name is left - off, ``main`` will be assumed. - - Example:: - - $ ptweens myapp.ini#main + usage = '%prog config_uri' + description = """\ + Print all implicit and explicit tween objects used by a Pyramid + application. The handler output includes whether the system is using an + explicit tweens ordering (will be true when the "pyramid.tweens" + deployment setting is used) or an implicit tweens ordering (will be true + when the "pyramid.tweens" deployment setting is *not* used). + + This command accepts one positional argument named "config_uri" which + specifies the PasteDeploy config file to use for the interactive + shell. The format is "inifile#name". If the name is left off, "main" + will be assumed. Example: "ptweens myapp.ini#main". """ - summary = "Print all tweens related to a Pyramid application" - stdout = sys.stdout - - parser = optparse.OptionParser() + parser = optparse.OptionParser( + usage, + description=textwrap.dedent(description), + ) + stdout = sys.stdout bootstrap = (bootstrap,) # testing def __init__(self, argv, quiet=False): @@ -61,7 +60,7 @@ class PTweensCommand(object): def run(self): if not self.args: self.out('Requires a config file argument') - return + return 2 config_uri = self.args[0] env = self.bootstrap[0](config_uri) registry = env['registry'] @@ -86,3 +85,4 @@ class PTweensCommand(object): self.out('Implicit Tween Chain') self.out('') self.show_chain(tweens.implicit()) + return 0 diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index 38d510542..72a9800c3 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -1,36 +1,34 @@ import optparse import sys +import textwrap from pyramid.interfaces import IMultiView from pyramid.paster import bootstrap def main(argv=sys.argv, quiet=False): command = PViewsCommand(argv, quiet) - command.run() + return command.run() class PViewsCommand(object): - """Print, for a given URL, the views that might match. Underneath each + usage = '%prog config_uri url' + description = """\ + Print, for a given URL, the views that might match. Underneath each potentially matching route, list the predicates required. Underneath each route+predicate set, print each view that might match and its predicates. - This command accepts two positional arguments: - - ``config_uri`` -- specifies the PasteDeploy config file to use for the - interactive shell. The format is ``inifile#name``. If the name is left - off, ``main`` will be assumed. - - ``url`` -- specifies the URL that will be used to find matching views. - - Example:: - - $ proutes myapp.ini#main url - + This command accepts two positional arguments: "config_uri" specifies the + PasteDeploy config file to use for the interactive shell. The format is + "inifile#name". If the name is left off, "main" will be assumed. "url" + specifies the path info portion of a URL that will be used to find + matching views. Example: "proutes myapp.ini#main /url" """ - summary = "Print all views in an application that might match a URL" stdout = sys.stdout - parser = optparse.OptionParser() + parser = optparse.OptionParser( + usage, + description=textwrap.dedent(description) + ) bootstrap = (bootstrap,) # testing @@ -231,7 +229,7 @@ class PViewsCommand(object): def run(self): if len(self.args) < 2: self.out('Command requires a config file arg and a url arg') - return + return 2 config_uri, url = self.args if not url.startswith('/'): url = '/%s' % url @@ -256,3 +254,5 @@ class PViewsCommand(object): else: self.out(" Not found.") self.out('') + return 0 + diff --git a/pyramid/tests/test_scaffolds/test_template.py b/pyramid/tests/test_scaffolds/test_template.py index e674a338d..d7cf638b6 100644 --- a/pyramid/tests/test_scaffolds/test_template.py +++ b/pyramid/tests/test_scaffolds/test_template.py @@ -7,34 +7,34 @@ class TestTemplate(unittest.TestCase): from pyramid.scaffolds.template import Template return Template(name) - def test_template_renderer_success(self): + def test_render_template_success(self): inst = self._makeOne() - result = inst.template_renderer('{{a}} {{b}}', {'a':'1', 'b':'2'}) + result = inst.render_template('{{a}} {{b}}', {'a':'1', 'b':'2'}) self.assertEqual(result, bytes_('1 2')) - def test_template_renderer_expr_failure(self): + def test_render_template_expr_failure(self): inst = self._makeOne() - self.assertRaises(AttributeError, inst.template_renderer, + self.assertRaises(AttributeError, inst.render_template, '{{a.foo}}', {'a':'1', 'b':'2'}) - def test_template_renderer_expr_success(self): + def test_render_template_expr_success(self): inst = self._makeOne() - result = inst.template_renderer('{{a.lower()}}', {'a':'A'}) + result = inst.render_template('{{a.lower()}}', {'a':'A'}) self.assertEqual(result, b'a') - def test_template_renderer_expr_success_via_pipe(self): + def test_render_template_expr_success_via_pipe(self): inst = self._makeOne() - result = inst.template_renderer('{{b|c|a.lower()}}', {'a':'A'}) + result = inst.render_template('{{b|c|a.lower()}}', {'a':'A'}) self.assertEqual(result, b'a') - def test_template_renderer_expr_success_via_pipe2(self): + def test_render_template_expr_success_via_pipe2(self): inst = self._makeOne() - result = inst.template_renderer('{{b|a.lower()|c}}', {'a':'A'}) + result = inst.render_template('{{b|a.lower()|c}}', {'a':'A'}) self.assertEqual(result, b'a') - def test_template_renderer_expr_value_is_None(self): + def test_render_template_expr_value_is_None(self): inst = self._makeOne() - result = inst.template_renderer('{{a}}', {'a':None}) + result = inst.render_template('{{a}}', {'a':None}) self.assertEqual(result, b'') def test_module_dir(self): @@ -83,7 +83,7 @@ class TestTemplate(unittest.TestCase): self.assertEqual(copydir.output_dir, 'output dir') self.assertEqual(copydir.vars, {'a':1}) self.assertEqual(copydir.kw, - {'template_renderer':inst.template_renderer, + {'template_renderer':inst.render_template, 'indent':1, 'verbosity':1, 'simulate':False, @@ -117,6 +117,10 @@ class TestTemplate(unittest.TestCase): inst.run(command, 'output dir', {'a':1}) self.assertEqual(L, ['output dir']) + def test_check_vars(self): + inst = self._makeOne() + self.assertRaises(RuntimeError, inst.check_vars, 'one', 'two') + class DummyCopydir(object): def copy_dir(self, template_dir, output_dir, vars, **kw): self.template_dir = template_dir diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index 363808a1e..1406d3911 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -22,7 +22,7 @@ class TestPCreateCommand(unittest.TestCase): def test_run_show_scaffolds_exist(self): cmd = self._makeOne('-l') result = cmd.run() - self.assertEqual(result, True) + self.assertEqual(result, 0) out = self.out_.getvalue() self.assertTrue(out.startswith('Available scaffolds')) @@ -30,14 +30,14 @@ class TestPCreateCommand(unittest.TestCase): cmd = self._makeOne('-l') cmd.scaffolds = [] result = cmd.run() - self.assertEqual(result, True) + self.assertEqual(result, 0) out = self.out_.getvalue() self.assertTrue(out.startswith('No scaffolds available')) def test_run_no_scaffold_name(self): cmd = self._makeOne() result = cmd.run() - self.assertEqual(result, None) + self.assertEqual(result, 2) out = self.out_.getvalue() self.assertTrue(out.startswith( 'You must provide at least one scaffold name')) @@ -45,14 +45,14 @@ class TestPCreateCommand(unittest.TestCase): def test_no_project_name(self): cmd = self._makeOne('-s', 'dummy') result = cmd.run() - self.assertEqual(result, None) + self.assertEqual(result, 2) out = self.out_.getvalue() self.assertTrue(out.startswith('You must provide a project name')) def test_unknown_scaffold_name(self): cmd = self._makeOne('-s', 'dummyXX', 'distro') result = cmd.run() - self.assertEqual(result, None) + self.assertEqual(result, 2) out = self.out_.getvalue() self.assertTrue(out.startswith('Unavailable scaffolds')) @@ -62,7 +62,7 @@ class TestPCreateCommand(unittest.TestCase): scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] result = cmd.run() - self.assertEqual(result, True) + self.assertEqual(result, 0) self.assertEqual( scaffold.output_dir, os.path.normpath(os.path.join(os.getcwd(), 'Distro')) @@ -78,7 +78,7 @@ class TestPCreateCommand(unittest.TestCase): scaffold = DummyScaffold('dummy') cmd.scaffolds = [scaffold] result = cmd.run() - self.assertEqual(result, True) + self.assertEqual(result, 0) self.assertEqual( scaffold.output_dir, os.path.normpath(os.path.join(os.getcwd(), 'Distro')) @@ -94,7 +94,7 @@ class TestPCreateCommand(unittest.TestCase): scaffold2 = DummyScaffold('dummy2') cmd.scaffolds = [scaffold1, scaffold2] result = cmd.run() - self.assertEqual(result, True) + self.assertEqual(result, 0) self.assertEqual( scaffold1.output_dir, os.path.normpath(os.path.join(os.getcwd(), 'Distro')) @@ -117,7 +117,7 @@ class Test_main(unittest.TestCase): def test_it(self): result = self._callFUT(['pcreate']) - self.assertEqual(result, None) + self.assertEqual(result, 2) class DummyScaffold(object): def __init__(self, name): 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/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 328d1001d..8c2660465 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -19,7 +19,7 @@ class TestPRoutesCommand(unittest.TestCase): L = [] command.out = L.append result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L, []) def test_no_mapper(self): @@ -28,7 +28,7 @@ class TestPRoutesCommand(unittest.TestCase): L = [] command.out = L.append result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L, []) def test_single_route_no_route_registered(self): @@ -39,7 +39,7 @@ class TestPRoutesCommand(unittest.TestCase): L = [] command.out = L.append result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(len(L), 3) self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>']) @@ -51,7 +51,7 @@ class TestPRoutesCommand(unittest.TestCase): L = [] command.out = L.append result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(len(L), 3) self.assertEqual(L[-1].split(), ['a', '/a', '<unknown>']) @@ -72,7 +72,7 @@ class TestPRoutesCommand(unittest.TestCase): command.out = L.append command.bootstrap = (dummy.DummyBootstrap(registry=registry),) result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(len(L), 3) self.assertEqual(L[-1].split()[:3], ['a', '/a', 'None']) @@ -98,7 +98,7 @@ class TestPRoutesCommand(unittest.TestCase): command.out = L.append command.bootstrap = (dummy.DummyBootstrap(registry=registry),) result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(len(L), 3) self.assertEqual(L[-1].split()[:4], ['a', '/a', '<function', 'view']) @@ -127,7 +127,7 @@ class TestPRoutesCommand(unittest.TestCase): command.out = L.append command.bootstrap = (dummy.DummyBootstrap(registry=registry),) result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(len(L), 3) self.assertEqual(L[-1].split()[:3], ['a', '/a', '<unknown>']) @@ -146,5 +146,5 @@ class Test_main(unittest.TestCase): def test_it(self): result = self._callFUT(['proutes']) - self.assertEqual(result, None) + self.assertEqual(result, 2) diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py index b181a7af2..fe489aa66 100644 --- a/pyramid/tests/test_scripts/test_pserve.py +++ b/pyramid/tests/test_scripts/test_pserve.py @@ -23,7 +23,7 @@ class TestPServeCommand(unittest.TestCase): def test_run_no_args(self): inst = self._makeOne() result = inst.run() - self.assertEqual(result, None) + self.assertEqual(result, 2) self.assertEqual(self.out_.getvalue(), 'You must give a config file') def test_run_stop_daemon_no_such_pid_file(self): @@ -73,7 +73,7 @@ class Test_main(unittest.TestCase): def test_it(self): result = self._callFUT(['pserve']) - self.assertEqual(result, None) + self.assertEqual(result, 2) class TestLazyWriter(unittest.TestCase): def _makeOne(self, filename, mode='w'): diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py index 765042152..8f9f3abfb 100644 --- a/pyramid/tests/test_scripts/test_pshell.py +++ b/pyramid/tests/test_scripts/test_pshell.py @@ -337,5 +337,5 @@ class Test_main(unittest.TestCase): def test_it(self): result = self._callFUT(['pshell']) - self.assertEqual(result, None) + self.assertEqual(result, 2) diff --git a/pyramid/tests/test_scripts/test_ptweens.py b/pyramid/tests/test_scripts/test_ptweens.py index 4dddaa0aa..f39f84b68 100644 --- a/pyramid/tests/test_scripts/test_ptweens.py +++ b/pyramid/tests/test_scripts/test_ptweens.py @@ -18,7 +18,7 @@ class TestPTweensCommand(unittest.TestCase): L = [] command.out = L.append result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L, []) def test_command_implicit_tweens_only(self): @@ -28,7 +28,7 @@ class TestPTweensCommand(unittest.TestCase): L = [] command.out = L.append result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual( L[0], '"pyramid.tweens" config value NOT set (implicitly ordered tweens ' @@ -41,7 +41,7 @@ class TestPTweensCommand(unittest.TestCase): L = [] command.out = L.append result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual( L[0], '"pyramid.tweens" config value set (explicitly ordered tweens used)') @@ -58,4 +58,4 @@ class Test_main(unittest.TestCase): def test_it(self): result = self._callFUT(['ptweens']) - self.assertEqual(result, None) + self.assertEqual(result, 2) diff --git a/pyramid/tests/test_scripts/test_pviews.py b/pyramid/tests/test_scripts/test_pviews.py index e2c3892fa..680d48cee 100644 --- a/pyramid/tests/test_scripts/test_pviews.py +++ b/pyramid/tests/test_scripts/test_pviews.py @@ -231,7 +231,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: None command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' Not found.') @@ -244,7 +244,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: None command.args = ('/foo/bar/myapp.ini#myapp', 'a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' Not found.') @@ -258,7 +258,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' context: context') self.assertEqual(L[4], ' view name: a') @@ -276,7 +276,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' context: context') self.assertEqual(L[4], ' view name: a') @@ -294,7 +294,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' context: context') self.assertEqual(L[4], ' view name: a') @@ -315,7 +315,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' context: context') self.assertEqual(L[4], ' view name: a') @@ -335,7 +335,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' context: context') self.assertEqual(L[4], ' view name: a') @@ -363,7 +363,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: multiview2 command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' context: context') self.assertEqual(L[4], ' view name: a') @@ -386,7 +386,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' context: context') self.assertEqual(L[4], ' view name: a') @@ -412,7 +412,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: multiview command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' context: context') self.assertEqual(L[4], ' view name: a') @@ -433,7 +433,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: multiview command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' context: context') self.assertEqual(L[4], ' view name: a') @@ -457,7 +457,7 @@ class TestPViewsCommand(unittest.TestCase): command._find_view = lambda arg1, arg2: multiview command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() - self.assertEqual(result, None) + self.assertEqual(result, 0) self.assertEqual(L[1], 'URL = /a') self.assertEqual(L[3], ' context: context') self.assertEqual(L[4], ' view name: a') @@ -472,4 +472,4 @@ class Test_main(unittest.TestCase): def test_it(self): result = self._callFUT(['pviews']) - self.assertEqual(result, None) + self.assertEqual(result, 2) @@ -56,7 +56,7 @@ if not PY3: ]) setup(name='pyramid', - version='1.4dev', + version='1.3a2', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, @@ -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 |
