diff options
48 files changed, 1915 insertions, 1378 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 916deb009..337754162 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,730 +4,28 @@ Next release Bug Fixes --------- -- When ``pyramid.wsgi.wsgiapp2`` calls the downstream WSGI app, the app's - environ will no longer have (deprecated and potentially misleading) - ``bfg.routes.matchdict`` or ``bfg.routes.route`` keys in it. A symptom of - this bug would be a ``wsgiapp2``-wrapped Pyramid app finding the wrong view - because it mistakenly detects that a route was matched when, in fact, it - was not. +- Forward port from 1.3 branch: When no authentication policy was configured, + a call to ``pyramid.security.effective_principals`` would unconditionally + return the empty list. This was incorrect, it should have unconditionally + returned ``[Everyone]``, and now does. -- The fix for issue https://github.com/Pylons/pyramid/issues/461 (which made - it possible for instance methods to be used as view callables) introduced a - backwards incompatibility when methods that declared only a request - argument were used. See https://github.com/Pylons/pyramid/issues/503 - -1.3b3 (2012-03-17) -================== - -Bug Fixes ---------- - -- ``config.add_view(<aninstancemethod>)`` raised AttributeError involving - ``__text__``. See https://github.com/Pylons/pyramid/issues/461 - -- Remove references to do-nothing ``pyramid.debug_templates`` setting in all - Pyramid-provided ``.ini`` files. This setting previously told Chameleon to - render better exceptions; now Chameleon always renders nice exceptions - regardless of the value of this setting. - -Scaffolds ---------- - -- The ``alchemy`` scaffold now shows an informative error message in the - browser if the person creating the project forgets to run the - initialization script. - -- The ``alchemy`` scaffold initialization script is now called - ``initialize_<projectname>_db`` instead of ``populate_<projectname>``. - -Documentation -------------- - -- Wiki tutorials improved due to collaboration at PyCon US 2012 sprints. - -1.3b2 (2012-03-02) -================== - -Bug Fixes ---------- - -- The method ``pyramid.request.Request.partial_application_url`` is no longer - in the API docs. It was meant to be a private method; its publication in - the documentation as an API method was a mistake, and it has been renamed - to something private. - -- When a static view was registered using an absolute filesystem path on - Windows, the ``request.static_url`` function did not work to generate URLs - to its resources. Symptom: "No static URL definition matching - c:\\foo\\bar\\baz". - -- Make all tests pass on Windows XP. - -- Bug in ACL authentication checking on Python 3: the ``permits`` and - ``principals_allowed_by_permission`` method of - ``pyramid.authorization.ACLAuthenticationPolicy`` could return an - inappropriate ``True`` value when a permission on an ACL was a string - rather than a sequence, and then only if the ACL permission string was a - substring of the ``permission`` value passed to the function. - - This bug effects no Pyramid deployment under Python 2; it is a bug that - exists only in deployments running on Python 3. It has existed since - Pyramid 1.3a1. - - This bug was due to the presence of an ``__iter__`` attribute on strings - under Python 3 which is not present under strings in Python 2. - -1.3b1 (2012-02-26) -================== - -Bug Fixes ---------- - -- ``pyramid.config.Configurator.with_package`` didn't work if the - Configurator was an old-style ``pyramid.configuration.Configurator`` - instance. - -- Pyramid authorization policies did not show up in the introspector. - -Deprecations ------------- - -- All references to the ``tmpl_context`` request variable were removed from - the docs. Its existence in Pyramid is confusing for people who were never - Pylons users. It was added as a porting convenience for Pylons users in - Pyramid 1.0, but it never caught on because the Pyramid rendering system is - a lot different than Pylons' was, and alternate ways exist to do what it - was designed to offer in Pylons. It will continue to exist "forever" but - it will not be recommended or mentioned in the docs. - -1.3a9 (2012-02-22) -================== - -Features --------- - -- Add an ``introspection`` boolean to the Configurator constructor. If this - is ``True``, actions registered using the Configurator will be registered - with the introspector. If it is ``False``, they won't. The default is - ``True``. Setting it to ``False`` during action processing will prevent - introspection for any following registration statements, and setting it to - ``True`` will start them up again. This addition is to service a - requirement that the debug toolbar's own views and methods not show up in - the introspector. - -- New API: ``pyramid.config.Configurator.add_notfound_view``. This is a - wrapper for ``pyramid.Config.configurator.add_view`` which provides easy - append_slash support and does the right thing about permissions. It should - be preferred over calling ``add_view`` directly with - ``context=HTTPNotFound`` as was previously recommended. - -- New API: ``pyramid.view.notfound_view_config``. This is a decorator - constructor like ``pyramid.view.view_config`` that calls - ``pyramid.config.Configurator.add_notfound_view`` when scanned. It should - be preferred over using ``pyramid.view.view_config`` with - ``context=HTTPNotFound`` as was previously recommended. - -- New API: ``pyramid.config.Configurator.add_forbidden_view``. This is a - wrapper for ``pyramid.Config.configurator.add_view`` which does the right - thing about permissions. It should be preferred over calling ``add_view`` - directly with ``context=HTTPForbidden`` as was previously recommended. - -- New API: ``pyramid.view.forbidden_view_config``. This is a decorator - constructor like ``pyramid.view.view_config`` that calls - ``pyramid.config.Configurator.add_forbidden_view`` when scanned. It should - be preferred over using ``pyramid.view.view_config`` with - ``context=HTTPForbidden`` as was previously recommended. - -- New APIs: ``pyramid.response.FileResponse`` and - ``pyramid.response.FileIter``, for usage in views that must serve files - "manually". - -Backwards Incompatibilities ---------------------------- - -- Remove ``pyramid.config.Configurator.with_context`` class method. It was - never an API, it is only used by ``pyramid_zcml`` and its functionality has - been moved to that package's latest release. This means that you'll need - to use the 0.9.2 or later release of ``pyramid_zcml`` with this release of - Pyramid. - -- The ``introspector`` argument to the ``pyramid.config.Configurator`` - constructor API has been removed. It has been replaced by the boolean - ``introspection`` flag. - -- The ``pyramid.registry.noop_introspector`` API object has been removed. - -- The older deprecated ``set_notfound_view`` Configurator method is now an - alias for the new ``add_notfound_view`` Configurator method. Likewise, the - older deprecated ``set_forbidden_view`` is now an alias for the new - ``add_forbidden_view``. This has the following impact: the ``context`` sent - to views with a ``(context, request)`` call signature registered via the - ``set_notfound_view`` or ``set_forbidden_view`` will now be an exception - object instead of the actual resource context found. Use - ``request.context`` to get the actual resource context. It's also - recommended to disuse ``set_notfound_view`` in favor of - ``add_notfound_view``, and disuse ``set_forbidden_view`` in favor of - ``add_forbidden_view`` despite the aliasing. - -Deprecations ------------- - -- The API documentation for ``pyramid.view.append_slash_notfound_view`` and - ``pyramid.view.AppendSlashNotFoundViewFactory`` was removed. These names - still exist and are still importable, but they are no longer APIs. Use - ``pyramid.config.Configurator.add_notfound_view(append_slash=True)`` or - ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same - behavior. - -- The ``set_forbidden_view`` and ``set_notfound_view`` methods of the - Configurator were removed from the documentation. They have been - deprecated since Pyramid 1.1. - -Bug Fixes ---------- - -- The static file response object used by ``config.add_static_view`` opened - the static file twice, when it only needed to open it once. - -- The AppendSlashNotFoundViewFactory used request.path to match routes. This - was wrong because request.path contains the script name, and this would - cause it to fail in circumstances where the script name was not empty. It - should have used request.path_info, and now does. - -Documentation -------------- - -- Updated the "Creating a Not Found View" section of the "Hooks" chapter, - replacing explanations of registering a view using ``add_view`` or - ``view_config`` with ones using ``add_notfound_view`` or - ``notfound_view_config``. - -- Updated the "Creating a Not Forbidden View" section of the "Hooks" chapter, - replacing explanations of registering a view using ``add_view`` or - ``view_config`` with ones using ``add_forbidden_view`` or - ``forbidden_view_config``. - -- Updated the "Redirecting to Slash-Appended Routes" section of the "URL - Dispatch" chapter, replacing explanations of registering a view using - ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or - ``notfound_view_config`` - -- Updated all tutorials to use ``pyramid.view.forbidden_view_config`` rather - than ``pyramid.view.view_config`` with an HTTPForbidden context. - -1.3a8 (2012-02-19) -================== - -Features --------- - -- The ``scan`` method of a ``Configurator`` can be passed an ``ignore`` - argument, which can be a string, a callable, or a list consisting of - strings and/or callables. This feature allows submodules, subpackages, and - global objects from being scanned. See - http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for - more information about how to use the ``ignore`` argument to ``scan``. - -- Better error messages when a view callable returns a value that cannot be - converted to a response (for example, when a view callable returns a - dictionary without a renderer defined, or doesn't return any value at all). - The error message now contains information about the view callable itself - as well as the result of calling it. - -- Better error message when a .pyc-only module is ``config.include`` -ed. - This is not permitted due to error reporting requirements, and a better - error message is shown when it is attempted. Previously it would fail with - something like "AttributeError: 'NoneType' object has no attribute - 'rfind'". - -- Add ``pyramid.config.Configurator.add_traverser`` API method. See the - Hooks narrative documentation section entitled "Changing the Traverser" for - more information. This is not a new feature, it just provides an API for - adding a traverser without needing to use the ZCA API. - -- Add ``pyramid.config.Configurator.add_resource_url_adapter`` API method. - See the Hooks narrative documentation section entitled "Changing How - pyramid.request.Request.resource_url Generates a URL" for more information. - This is not a new feature, it just provides an API for adding a resource - url adapter without needing to use the ZCA API. - -- The system value ``req`` is now supplied to renderers as an alias for - ``request``. This means that you can now, for example, in a template, do - ``req.route_url(...)`` instead of ``request.route_url(...)``. This is - purely a change to reduce the amount of typing required to use request - methods and attributes from within templates. The value ``request`` is - still available too, this is just an alternative. - -- A new interface was added: ``pyramid.interfaces.IResourceURL``. An adapter - implementing its interface can be used to override resource URL generation - when ``request.resource_url`` is called. This interface replaces the - now-deprecated ``pyramid.interfaces.IContextURL`` interface. - -- The dictionary passed to a resource's ``__resource_url__`` method (see - "Overriding Resource URL Generation" in the "Resources" chapter) now - contains an ``app_url`` key, representing the application URL generated - during ``request.resource_url``. It represents a potentially customized - URL prefix, containing potentially custom scheme, host and port information - passed by the user to ``request.resource_url``. It should be used instead - of ``request.application_url`` where necessary. - -- The ``request.resource_url`` API now accepts these arguments: ``app_url``, - ``scheme``, ``host``, and ``port``. The app_url argument can be used to - replace the URL prefix wholesale during url generation. The ``scheme``, - ``host``, and ``port`` arguments can be used to replace the respective - default values of ``request.application_url`` partially. - -- A new API named ``request.resource_path`` now exists. It works like - ``request.resource_url`` but produces a relative URL rather than an - absolute one. - -- The ``request.route_url`` API now accepts these arguments: ``_app_url``, - ``_scheme``, ``_host``, and ``_port``. The ``_app_url`` argument can be - used to replace the URL prefix wholesale during url generation. The - ``_scheme``, ``_host``, and ``_port`` arguments can be used to replace the - respective default values of ``request.application_url`` partially. - -Backwards Incompatibilities ---------------------------- - -- The ``pyramid.interfaces.IContextURL`` interface has been deprecated. - People have been instructed to use this to register a resource url adapter - in the "Hooks" chapter to use to influence ``request.resource_url`` URL - generation for resources found via custom traversers since Pyramid 1.0. - - The interface still exists and registering such an adapter still works, but - this interface will be removed from the software after a few major Pyramid - releases. You should replace it with an equivalent - ``pyramid.interfaces.IResourceURL`` adapter, registered using the new - ``pyramid.config.Configurator.add_resource_url_adapter`` API. A - deprecation warning is now emitted when a - ``pyramid.interfaces.IContextURL`` adapter is found when - ``request.resource_url`` is called. - -Documentation -------------- - -- Don't create a ``session`` instance in SQLA Wiki tutorial, use raw - ``DBSession`` instead (this is more common in real SQLA apps). - -Scaffolding ------------ - -- Put ``pyramid.includes`` targets within ini files in scaffolds on separate - lines in order to be able to tell people to comment out only the - ``pyramid_debugtoolbar`` line when they want to disable the toolbar. - -Dependencies ------------- - -- Depend on ``venusian`` >= 1.0a3 to provide scan ``ignore`` support. - -Internal --------- - -- Create a "MakoRendererFactoryHelper" that provides customizable settings - key prefixes. Allows settings prefixes other than "mako." to be used to - create different factories that don't use the global mako settings. This - will be useful for the debug toolbar, which can currently be sabotaged by - someone using custom mako configuration settings. - -1.3a7 (2012-02-07) -================== - -Features --------- - -- More informative error message when a ``config.include`` cannot find an - ``includeme``. See https://github.com/Pylons/pyramid/pull/392. - -- Internal: catch unhashable discriminators early (raise an error instead of - allowing them to find their way into resolveConflicts). - -- The `match_param` view predicate now accepts a string or a tuple. - This replaces the broken behavior of accepting a dict. See - https://github.com/Pylons/pyramid/issues/425 for more information. - -Bug Fixes ---------- - -- The process will now restart when ``pserve`` is used with the ``--reload`` - flag when the ``development.ini`` file (or any other .ini file in use) is - changed. See https://github.com/Pylons/pyramid/issues/377 and - https://github.com/Pylons/pyramid/pull/411 - -- The ``prequest`` script would fail when used against URLs which did not - return HTML or text. See https://github.com/Pylons/pyramid/issues/381 - -Backwards Incompatibilities ---------------------------- - -- The `match_param` view predicate no longer accepts a dict. This will - have no negative affect because the implementation was broken for - dict-based arguments. - -Documentation -------------- - -- Add a traversal hello world example to the narrative docs. - -1.3a6 (2012-01-20) -================== - -Features --------- - -- New API: ``pyramid.config.Configurator.set_request_property``. Add lazy - property descriptors to a request without changing the request factory. - This method provides conflict detection and is the suggested way to add - properties to a request. - -- Responses generated by Pyramid's ``static_view`` now use - a ``wsgi.file_wrapper`` (see - http://www.python.org/dev/peps/pep-0333/#optional-platform-specific-file-handling) - when one is provided by the web server. - -Bug Fixes ---------- - -- Views registered with an ``accept`` could not be overridden correctly with - a different view that had the same predicate arguments. See - https://github.com/Pylons/pyramid/pull/404 for more information. - -- When using a dotted name for a ``view`` argument to - ``Configurator.add_view`` that pointed to a class with a ``view_defaults`` - decorator, the view defaults would not be applied. See - https://github.com/Pylons/pyramid/issues/396 . - -- Static URL paths were URL-quoted twice. See - https://github.com/Pylons/pyramid/issues/407 . - -1.3a5 (2012-01-09) -================== - -Bug Fixes ---------- - -- The ``pyramid.view.view_defaults`` decorator did not work properly when - more than one view relied on the defaults being different for configuration - conflict resolution. See https://github.com/Pylons/pyramid/issues/394. - -Backwards Incompatibilities ---------------------------- - -- The ``path_info`` route and view predicates now match against - ``request.upath_info`` (Unicode) rather than ``request.path_info`` - (indeterminate value based on Python 3 vs. Python 2). This has to be done - to normalize matching on Python 2 and Python 3. - -1.3a4 (2012-01-05) -================== - -Features --------- - -- New API: ``pyramid.request.Request.set_property``. Add lazy property - descriptors to a request without changing the request factory. New - properties may be reified, effectively caching the value for the lifetime - of the instance. Common use-cases for this would be to get a database - connection for the request or identify the current user. - -- Use the ``waitress`` WSGI server instead of ``wsgiref`` in scaffolding. - -Bug Fixes ---------- - -- The documentation of ``pyramid.events.subscriber`` indicated that using it - as a decorator with no arguments like this:: - - @subscriber() - def somefunc(event): - pass - - Would register ``somefunc`` to receive all events sent via the registry, - but this was untrue. Instead, it would receive no events at all. This has - now been fixed and the code matches the documentation. See also - https://github.com/Pylons/pyramid/issues/386 - -- Literal portions of route patterns were not URL-quoted when ``route_url`` - or ``route_path`` was used to generate a URL or path. - -- The result of ``route_path`` or ``route_url`` might have been ``unicode`` - or ``str`` depending on the input. It is now guaranteed to always be - ``str``. - -- URL matching when the pattern contained non-ASCII characters in literal - parts was indeterminate. Now the pattern supplied to ``add_route`` is - assumed to be either: a ``unicode`` value, or a ``str`` value that contains - only ASCII characters. If you now want to match the path info from a URL - that contains high order characters, you can pass the Unicode - representation of the decoded path portion in the pattern. - -- When using a ``traverse=`` route predicate, traversal would fail with a - URLDecodeError if there were any high-order characters in the traversal - pattern or in the matched dynamic segments. - -- Using a dynamic segment named ``traverse`` in a route pattern like this:: - - config.add_route('trav_route', 'traversal/{traverse:.*}') - - Would cause a ``UnicodeDecodeError`` when the route was matched and the - matched portion of the URL contained any high-order characters. See - https://github.com/Pylons/pyramid/issues/385 . - -- When using a ``*traverse`` stararg in a route pattern, a URL that matched - that possessed a ``@@`` in its name (signifying a view name) would be - inappropriately quoted by the traversal machinery during traversal, - resulting in the view not being found properly. See - https://github.com/Pylons/pyramid/issues/382 and - https://github.com/Pylons/pyramid/issues/375 . - -Backwards Incompatibilities ---------------------------- - -- String values passed to ``route_url`` or ``route_path`` that are meant to - replace "remainder" matches will now be URL-quoted except for embedded - slashes. For example:: - - config.add_route('remain', '/foo*remainder') - request.route_path('remain', remainder='abc / def') - # -> '/foo/abc%20/%20def' - - Previously string values passed as remainder replacements were tacked on - untouched, without any URL-quoting. But this doesn't really work logically - if the value passed is Unicode (raw unicode cannot be placed in a URL or in - a path) and it is inconsistent with the rest of the URL generation - machinery if the value is a string (it won't be quoted unless by the - caller). - - Some folks will have been relying on the older behavior to tack on query - string elements and anchor portions of the URL; sorry, you'll need to - change your code to use the ``_query`` and/or ``_anchor`` arguments to - ``route_path`` or ``route_url`` to do this now. - -- If you pass a bytestring that contains non-ASCII characters to - ``add_route`` as a pattern, it will now fail at startup time. Use Unicode - instead. - -1.3a3 (2011-12-21) -================== 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". - -- Add undocumented ``__discriminator__`` API to derived view callables. - e.g. ``adapters.lookup(...).__discriminator__(context, request)``. It will - be used by superdynamic systems that require the discriminator to be used - for introspection after manual view lookup. - -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. - -- Removed the "Running Pyramid on Google App Engine" tutorial from the main - docs. It survives on in the Cookbook - (http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/gae.html). - Rationale: it provides the correct info for the Python 2.5 version of GAE - only, and this version of Pyramid does not support Python 2.5. - -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 -------------- - -- Added documentation to "View Configuration" narrative documentation chapter - about ``view_defaults`` class decorator. - -- 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) -================== - -Features --------- - -- Python 3.2 compatibility. - -- New ``pyramid.compat`` module and API documentation which provides Python - 2/3 straddling support for Pyramid add-ons and development environments. - -- A ``mako.directories`` setting is no longer required to use Mako templates - Rationale: Mako template renderers can be specified using an absolute asset - spec. An entire application can be written with such asset specs, - requiring no ordered lookup path. - -- ``bpython`` interpreter compatibility in ``pshell``. See the "Command-Line - Pyramid" narrative docs chapter for more information. - -- Added ``get_appsettings`` API function to the ``pyramid.paster`` module. - This function returns the settings defined within an ``[app:...]`` section - in a PasteDeploy ini file. - -- Added ``setup_logging`` API function to the ``pyramid.paster`` module. - This function sets up Python logging according to the logging configuration - in a PasteDeploy ini file. - -- Configuration conflict reporting is reported in a more understandable way - ("Line 11 in file..." vs. a repr of a tuple of similar info). - -- A configuration introspection system was added; see the narrative - documentation chapter entitled "Pyramid Configuration Introspection" for - more information. New APIs: ``pyramid.registry.Introspectable``, - ``pyramid.config.Configurator.introspector``, - ``pyramid.config.Configurator.introspectable``, - ``pyramid.registry.Registry.introspector``. - -- Allow extra keyword arguments to be passed to the - ``pyramid.config.Configurator.action`` method. - -- New APIs: ``pyramid.path.AssetResolver`` and - ``pyramid.path.DottedNameResolver``. The former can be used to resolve - asset specifications, the latter can be used to resolve dotted names to - modules or packages. - -Bug Fixes ---------- - -- Make test suite pass on 32-bit systems; closes #286. closes #306. - See also https://github.com/Pylons/pyramid/issues/286 - -- The ``pryamid.view.view_config`` decorator did not accept a ``match_params`` - predicate argument. See https://github.com/Pylons/pyramid/pull/308 - -- The AuthTktCookieHelper could potentially generate Unicode headers - inappropriately when the ``tokens`` argument to remember was used. See - https://github.com/Pylons/pyramid/pull/314. - -- The AuthTktAuthenticationPolicy did not use a timing-attack-aware string - comparator. See https://github.com/Pylons/pyramid/pull/320 for more info. - -- The DummySession in ``pyramid.testing`` now generates a new CSRF token if - one doesn't yet exist. - -- ``request.static_url`` now generates URL-quoted URLs when fed a ``path`` - argument which contains characters that are unsuitable for URLs. See - https://github.com/Pylons/pyramid/issues/349 for more info. - -- Prevent a scaffold rendering from being named ``site`` (conflicts with - Python internal site.py). - -- Support for using instances as targets of the ``pyramid.wsgi.wsgiapp`` and - ``pryramid.wsgi.wsgiapp2`` functions. - See https://github.com/Pylons/pyramid/pull/370 for more info. - -Backwards Incompatibilities ---------------------------- - -- Pyramid no longer runs on Python 2.5 (which includes the most recent - release of Jython and the Python 2.5 version of GAE as of this writing). - -- The ``paster`` command is no longer the documented way to create projects, - start the server, or run debugging commands. To create projects from - scaffolds, ``paster create`` is replaced by the ``pcreate`` console script. - To serve up a project, ``paster serve`` is replaced by the ``pserve`` - console script. New console scripts named ``pshell``, ``pviews``, - ``proutes``, and ``ptweens`` do what their ``paster <commandname>`` - equivalents used to do. Rationale: the Paste and PasteScript packages do - not run under Python 3. - -- The default WSGI server run as the result of ``pserve`` from newly rendered - scaffolding is now the ``wsgiref`` WSGI server instead of the - ``paste.httpserver`` server. Rationale: Rationale: the Paste and - PasteScript packages do not run under Python 3. - -- The ``pshell`` command (see "paster pshell") no longer accepts a - ``--disable-ipython`` command-line argument. Instead, it accepts a ``-p`` - or ``--python-shell`` argument, which can be any of the values ``python``, - ``ipython`` or ``bpython``. - -- Removed the ``pyramid.renderers.renderer_from_name`` function. It has been - deprecated since Pyramid 1.0, and was never an API. - -- To use ZCML with versions of Pyramid >= 1.3, you will need ``pyramid_zcml`` - version >= 0.8 and ``zope.configuration`` version >= 3.8.0. The - ``pyramid_zcml`` package version 0.8 is backwards compatible all the way to - Pyramid 1.0, so you won't be warned if you have older versions installed - and upgrade Pyramid "in-place"; it may simply break instead. - -Dependencies ------------- - -- Pyramid no longer depends on the ``zope.component`` package, except as a - testing dependency. - -- Pyramid now depends on a zope.interface>=3.8.0, WebOb>=1.2dev, - repoze.lru>=0.4, zope.deprecation>=3.5.0, translationstring>=0.4 (for - Python 3 compatibility purposes). It also, as a testing dependency, - depends on WebTest>=1.3.1 for the same reason. - -- Pyramid no longer depends on the Paste or PasteScript packages. - -Documentation -------------- - -- The SQLAlchemy Wiki tutorial has been updated. It now uses - ``@view_config`` decorators and an explicit database population script. - -- Minor updates to the ZODB Wiki tutorial. - -- A narrative documentation chapter named "Extending Pyramid Configuration" - was added; it describes how to add a new directive, and how use the - ``pyramid.config.Configurator.action`` method within custom directives. It - also describes how to add introspectable objects. - -- A narrative documentation chapter named "Pyramid Configuration - Introspection" was added. It describes how to query the introspection - system. - -Scaffolds ---------- - -- Rendered scaffolds have now been changed to be more relocatable (fewer - mentions of the package name within files in the package). - -- The ``routesalchemy`` scaffold has been renamed ``alchemy``, replacing the - older (traversal-based) ``alchemy`` scaffold (which has been retired). +- Custom objects can be made easily JSON-serializable in Pyramid by defining + a ``__json__`` method on the object's class. This method should return + values natively serializable by ``json.dumps`` (such as ints, lists, + dictionaries, strings, and so forth). -- The ``starter`` scaffold now uses URL dispatch by default. +- As of this release, the ``request_method`` predicate, when used, will also + imply that ``HEAD`` is implied when you use ``GET``. For example, using + ``@view_config(request_method='GET')`` is equivalent to using + ``@view_config(request_method='HEAD')``. Using + ``@view_config(request_method=('GET', 'POST')`` is equivalent to using + ``@view_config(request_method=('GET', 'HEAD', 'POST')``. This is because + HEAD is a variant of GET that omits the body, and WebOb has special support + to return an empty body when a HEAD is used. +- ``config.set_request_property`` now causes less code to be executed at + request construction time. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index a402d49e6..4b780d3a7 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -127,7 +127,7 @@ Contributors - Wichert Akkerman, 2011/01/19 -- Christopher Lambacehr, 2011/02/12 +- Christopher Lambacher, 2011/02/12 - Malthe Borch, 2011/02/28 @@ -166,3 +166,7 @@ Contributors - Paul M. Winkler, 2012/02/22 - Martijn Pieters, 2012/03/02 + +- Steve Piercy, 2012/03/27 + +- Wayne Witzel III, 2012/03/27 diff --git a/HISTORY.txt b/HISTORY.txt index f6cf8fa87..f10cfa3ab 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -1,3 +1,736 @@ +1.3 (2012-03-21) +================ + +Bug Fixes +--------- + +- When ``pyramid.wsgi.wsgiapp2`` calls the downstream WSGI app, the app's + environ will no longer have (deprecated and potentially misleading) + ``bfg.routes.matchdict`` or ``bfg.routes.route`` keys in it. A symptom of + this bug would be a ``wsgiapp2``-wrapped Pyramid app finding the wrong view + because it mistakenly detects that a route was matched when, in fact, it + was not. + +- The fix for issue https://github.com/Pylons/pyramid/issues/461 (which made + it possible for instance methods to be used as view callables) introduced a + backwards incompatibility when methods that declared only a request + argument were used. See https://github.com/Pylons/pyramid/issues/503 + +1.3b3 (2012-03-17) +================== + +Bug Fixes +--------- + +- ``config.add_view(<aninstancemethod>)`` raised AttributeError involving + ``__text__``. See https://github.com/Pylons/pyramid/issues/461 + +- Remove references to do-nothing ``pyramid.debug_templates`` setting in all + Pyramid-provided ``.ini`` files. This setting previously told Chameleon to + render better exceptions; now Chameleon always renders nice exceptions + regardless of the value of this setting. + +Scaffolds +--------- + +- The ``alchemy`` scaffold now shows an informative error message in the + browser if the person creating the project forgets to run the + initialization script. + +- The ``alchemy`` scaffold initialization script is now called + ``initialize_<projectname>_db`` instead of ``populate_<projectname>``. + +Documentation +------------- + +- Wiki tutorials improved due to collaboration at PyCon US 2012 sprints. + +1.3b2 (2012-03-02) +================== + +Bug Fixes +--------- + +- The method ``pyramid.request.Request.partial_application_url`` is no longer + in the API docs. It was meant to be a private method; its publication in + the documentation as an API method was a mistake, and it has been renamed + to something private. + +- When a static view was registered using an absolute filesystem path on + Windows, the ``request.static_url`` function did not work to generate URLs + to its resources. Symptom: "No static URL definition matching + c:\\foo\\bar\\baz". + +- Make all tests pass on Windows XP. + +- Bug in ACL authentication checking on Python 3: the ``permits`` and + ``principals_allowed_by_permission`` method of + ``pyramid.authorization.ACLAuthenticationPolicy`` could return an + inappropriate ``True`` value when a permission on an ACL was a string + rather than a sequence, and then only if the ACL permission string was a + substring of the ``permission`` value passed to the function. + + This bug effects no Pyramid deployment under Python 2; it is a bug that + exists only in deployments running on Python 3. It has existed since + Pyramid 1.3a1. + + This bug was due to the presence of an ``__iter__`` attribute on strings + under Python 3 which is not present under strings in Python 2. + +1.3b1 (2012-02-26) +================== + +Bug Fixes +--------- + +- ``pyramid.config.Configurator.with_package`` didn't work if the + Configurator was an old-style ``pyramid.configuration.Configurator`` + instance. + +- Pyramid authorization policies did not show up in the introspector. + +Deprecations +------------ + +- All references to the ``tmpl_context`` request variable were removed from + the docs. Its existence in Pyramid is confusing for people who were never + Pylons users. It was added as a porting convenience for Pylons users in + Pyramid 1.0, but it never caught on because the Pyramid rendering system is + a lot different than Pylons' was, and alternate ways exist to do what it + was designed to offer in Pylons. It will continue to exist "forever" but + it will not be recommended or mentioned in the docs. + +1.3a9 (2012-02-22) +================== + +Features +-------- + +- Add an ``introspection`` boolean to the Configurator constructor. If this + is ``True``, actions registered using the Configurator will be registered + with the introspector. If it is ``False``, they won't. The default is + ``True``. Setting it to ``False`` during action processing will prevent + introspection for any following registration statements, and setting it to + ``True`` will start them up again. This addition is to service a + requirement that the debug toolbar's own views and methods not show up in + the introspector. + +- New API: ``pyramid.config.Configurator.add_notfound_view``. This is a + wrapper for ``pyramid.Config.configurator.add_view`` which provides easy + append_slash support and does the right thing about permissions. It should + be preferred over calling ``add_view`` directly with + ``context=HTTPNotFound`` as was previously recommended. + +- New API: ``pyramid.view.notfound_view_config``. This is a decorator + constructor like ``pyramid.view.view_config`` that calls + ``pyramid.config.Configurator.add_notfound_view`` when scanned. It should + be preferred over using ``pyramid.view.view_config`` with + ``context=HTTPNotFound`` as was previously recommended. + +- New API: ``pyramid.config.Configurator.add_forbidden_view``. This is a + wrapper for ``pyramid.Config.configurator.add_view`` which does the right + thing about permissions. It should be preferred over calling ``add_view`` + directly with ``context=HTTPForbidden`` as was previously recommended. + +- New API: ``pyramid.view.forbidden_view_config``. This is a decorator + constructor like ``pyramid.view.view_config`` that calls + ``pyramid.config.Configurator.add_forbidden_view`` when scanned. It should + be preferred over using ``pyramid.view.view_config`` with + ``context=HTTPForbidden`` as was previously recommended. + +- New APIs: ``pyramid.response.FileResponse`` and + ``pyramid.response.FileIter``, for usage in views that must serve files + "manually". + +Backwards Incompatibilities +--------------------------- + +- Remove ``pyramid.config.Configurator.with_context`` class method. It was + never an API, it is only used by ``pyramid_zcml`` and its functionality has + been moved to that package's latest release. This means that you'll need + to use the 0.9.2 or later release of ``pyramid_zcml`` with this release of + Pyramid. + +- The ``introspector`` argument to the ``pyramid.config.Configurator`` + constructor API has been removed. It has been replaced by the boolean + ``introspection`` flag. + +- The ``pyramid.registry.noop_introspector`` API object has been removed. + +- The older deprecated ``set_notfound_view`` Configurator method is now an + alias for the new ``add_notfound_view`` Configurator method. Likewise, the + older deprecated ``set_forbidden_view`` is now an alias for the new + ``add_forbidden_view``. This has the following impact: the ``context`` sent + to views with a ``(context, request)`` call signature registered via the + ``set_notfound_view`` or ``set_forbidden_view`` will now be an exception + object instead of the actual resource context found. Use + ``request.context`` to get the actual resource context. It's also + recommended to disuse ``set_notfound_view`` in favor of + ``add_notfound_view``, and disuse ``set_forbidden_view`` in favor of + ``add_forbidden_view`` despite the aliasing. + +Deprecations +------------ + +- The API documentation for ``pyramid.view.append_slash_notfound_view`` and + ``pyramid.view.AppendSlashNotFoundViewFactory`` was removed. These names + still exist and are still importable, but they are no longer APIs. Use + ``pyramid.config.Configurator.add_notfound_view(append_slash=True)`` or + ``pyramid.view.notfound_view_config(append_slash=True)`` to get the same + behavior. + +- The ``set_forbidden_view`` and ``set_notfound_view`` methods of the + Configurator were removed from the documentation. They have been + deprecated since Pyramid 1.1. + +Bug Fixes +--------- + +- The static file response object used by ``config.add_static_view`` opened + the static file twice, when it only needed to open it once. + +- The AppendSlashNotFoundViewFactory used request.path to match routes. This + was wrong because request.path contains the script name, and this would + cause it to fail in circumstances where the script name was not empty. It + should have used request.path_info, and now does. + +Documentation +------------- + +- Updated the "Creating a Not Found View" section of the "Hooks" chapter, + replacing explanations of registering a view using ``add_view`` or + ``view_config`` with ones using ``add_notfound_view`` or + ``notfound_view_config``. + +- Updated the "Creating a Not Forbidden View" section of the "Hooks" chapter, + replacing explanations of registering a view using ``add_view`` or + ``view_config`` with ones using ``add_forbidden_view`` or + ``forbidden_view_config``. + +- Updated the "Redirecting to Slash-Appended Routes" section of the "URL + Dispatch" chapter, replacing explanations of registering a view using + ``add_view`` or ``view_config`` with ones using ``add_notfound_view`` or + ``notfound_view_config`` + +- Updated all tutorials to use ``pyramid.view.forbidden_view_config`` rather + than ``pyramid.view.view_config`` with an HTTPForbidden context. + +1.3a8 (2012-02-19) +================== + +Features +-------- + +- The ``scan`` method of a ``Configurator`` can be passed an ``ignore`` + argument, which can be a string, a callable, or a list consisting of + strings and/or callables. This feature allows submodules, subpackages, and + global objects from being scanned. See + http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for + more information about how to use the ``ignore`` argument to ``scan``. + +- Better error messages when a view callable returns a value that cannot be + converted to a response (for example, when a view callable returns a + dictionary without a renderer defined, or doesn't return any value at all). + The error message now contains information about the view callable itself + as well as the result of calling it. + +- Better error message when a .pyc-only module is ``config.include`` -ed. + This is not permitted due to error reporting requirements, and a better + error message is shown when it is attempted. Previously it would fail with + something like "AttributeError: 'NoneType' object has no attribute + 'rfind'". + +- Add ``pyramid.config.Configurator.add_traverser`` API method. See the + Hooks narrative documentation section entitled "Changing the Traverser" for + more information. This is not a new feature, it just provides an API for + adding a traverser without needing to use the ZCA API. + +- Add ``pyramid.config.Configurator.add_resource_url_adapter`` API method. + See the Hooks narrative documentation section entitled "Changing How + pyramid.request.Request.resource_url Generates a URL" for more information. + This is not a new feature, it just provides an API for adding a resource + url adapter without needing to use the ZCA API. + +- The system value ``req`` is now supplied to renderers as an alias for + ``request``. This means that you can now, for example, in a template, do + ``req.route_url(...)`` instead of ``request.route_url(...)``. This is + purely a change to reduce the amount of typing required to use request + methods and attributes from within templates. The value ``request`` is + still available too, this is just an alternative. + +- A new interface was added: ``pyramid.interfaces.IResourceURL``. An adapter + implementing its interface can be used to override resource URL generation + when ``request.resource_url`` is called. This interface replaces the + now-deprecated ``pyramid.interfaces.IContextURL`` interface. + +- The dictionary passed to a resource's ``__resource_url__`` method (see + "Overriding Resource URL Generation" in the "Resources" chapter) now + contains an ``app_url`` key, representing the application URL generated + during ``request.resource_url``. It represents a potentially customized + URL prefix, containing potentially custom scheme, host and port information + passed by the user to ``request.resource_url``. It should be used instead + of ``request.application_url`` where necessary. + +- The ``request.resource_url`` API now accepts these arguments: ``app_url``, + ``scheme``, ``host``, and ``port``. The app_url argument can be used to + replace the URL prefix wholesale during url generation. The ``scheme``, + ``host``, and ``port`` arguments can be used to replace the respective + default values of ``request.application_url`` partially. + +- A new API named ``request.resource_path`` now exists. It works like + ``request.resource_url`` but produces a relative URL rather than an + absolute one. + +- The ``request.route_url`` API now accepts these arguments: ``_app_url``, + ``_scheme``, ``_host``, and ``_port``. The ``_app_url`` argument can be + used to replace the URL prefix wholesale during url generation. The + ``_scheme``, ``_host``, and ``_port`` arguments can be used to replace the + respective default values of ``request.application_url`` partially. + +Backwards Incompatibilities +--------------------------- + +- The ``pyramid.interfaces.IContextURL`` interface has been deprecated. + People have been instructed to use this to register a resource url adapter + in the "Hooks" chapter to use to influence ``request.resource_url`` URL + generation for resources found via custom traversers since Pyramid 1.0. + + The interface still exists and registering such an adapter still works, but + this interface will be removed from the software after a few major Pyramid + releases. You should replace it with an equivalent + ``pyramid.interfaces.IResourceURL`` adapter, registered using the new + ``pyramid.config.Configurator.add_resource_url_adapter`` API. A + deprecation warning is now emitted when a + ``pyramid.interfaces.IContextURL`` adapter is found when + ``request.resource_url`` is called. + +Documentation +------------- + +- Don't create a ``session`` instance in SQLA Wiki tutorial, use raw + ``DBSession`` instead (this is more common in real SQLA apps). + +Scaffolding +----------- + +- Put ``pyramid.includes`` targets within ini files in scaffolds on separate + lines in order to be able to tell people to comment out only the + ``pyramid_debugtoolbar`` line when they want to disable the toolbar. + +Dependencies +------------ + +- Depend on ``venusian`` >= 1.0a3 to provide scan ``ignore`` support. + +Internal +-------- + +- Create a "MakoRendererFactoryHelper" that provides customizable settings + key prefixes. Allows settings prefixes other than "mako." to be used to + create different factories that don't use the global mako settings. This + will be useful for the debug toolbar, which can currently be sabotaged by + someone using custom mako configuration settings. + +1.3a7 (2012-02-07) +================== + +Features +-------- + +- More informative error message when a ``config.include`` cannot find an + ``includeme``. See https://github.com/Pylons/pyramid/pull/392. + +- Internal: catch unhashable discriminators early (raise an error instead of + allowing them to find their way into resolveConflicts). + +- The `match_param` view predicate now accepts a string or a tuple. + This replaces the broken behavior of accepting a dict. See + https://github.com/Pylons/pyramid/issues/425 for more information. + +Bug Fixes +--------- + +- The process will now restart when ``pserve`` is used with the ``--reload`` + flag when the ``development.ini`` file (or any other .ini file in use) is + changed. See https://github.com/Pylons/pyramid/issues/377 and + https://github.com/Pylons/pyramid/pull/411 + +- The ``prequest`` script would fail when used against URLs which did not + return HTML or text. See https://github.com/Pylons/pyramid/issues/381 + +Backwards Incompatibilities +--------------------------- + +- The `match_param` view predicate no longer accepts a dict. This will + have no negative affect because the implementation was broken for + dict-based arguments. + +Documentation +------------- + +- Add a traversal hello world example to the narrative docs. + +1.3a6 (2012-01-20) +================== + +Features +-------- + +- New API: ``pyramid.config.Configurator.set_request_property``. Add lazy + property descriptors to a request without changing the request factory. + This method provides conflict detection and is the suggested way to add + properties to a request. + +- Responses generated by Pyramid's ``static_view`` now use + a ``wsgi.file_wrapper`` (see + http://www.python.org/dev/peps/pep-0333/#optional-platform-specific-file-handling) + when one is provided by the web server. + +Bug Fixes +--------- + +- Views registered with an ``accept`` could not be overridden correctly with + a different view that had the same predicate arguments. See + https://github.com/Pylons/pyramid/pull/404 for more information. + +- When using a dotted name for a ``view`` argument to + ``Configurator.add_view`` that pointed to a class with a ``view_defaults`` + decorator, the view defaults would not be applied. See + https://github.com/Pylons/pyramid/issues/396 . + +- Static URL paths were URL-quoted twice. See + https://github.com/Pylons/pyramid/issues/407 . + +1.3a5 (2012-01-09) +================== + +Bug Fixes +--------- + +- The ``pyramid.view.view_defaults`` decorator did not work properly when + more than one view relied on the defaults being different for configuration + conflict resolution. See https://github.com/Pylons/pyramid/issues/394. + +Backwards Incompatibilities +--------------------------- + +- The ``path_info`` route and view predicates now match against + ``request.upath_info`` (Unicode) rather than ``request.path_info`` + (indeterminate value based on Python 3 vs. Python 2). This has to be done + to normalize matching on Python 2 and Python 3. + +1.3a4 (2012-01-05) +================== + +Features +-------- + +- New API: ``pyramid.request.Request.set_property``. Add lazy property + descriptors to a request without changing the request factory. New + properties may be reified, effectively caching the value for the lifetime + of the instance. Common use-cases for this would be to get a database + connection for the request or identify the current user. + +- Use the ``waitress`` WSGI server instead of ``wsgiref`` in scaffolding. + +Bug Fixes +--------- + +- The documentation of ``pyramid.events.subscriber`` indicated that using it + as a decorator with no arguments like this:: + + @subscriber() + def somefunc(event): + pass + + Would register ``somefunc`` to receive all events sent via the registry, + but this was untrue. Instead, it would receive no events at all. This has + now been fixed and the code matches the documentation. See also + https://github.com/Pylons/pyramid/issues/386 + +- Literal portions of route patterns were not URL-quoted when ``route_url`` + or ``route_path`` was used to generate a URL or path. + +- The result of ``route_path`` or ``route_url`` might have been ``unicode`` + or ``str`` depending on the input. It is now guaranteed to always be + ``str``. + +- URL matching when the pattern contained non-ASCII characters in literal + parts was indeterminate. Now the pattern supplied to ``add_route`` is + assumed to be either: a ``unicode`` value, or a ``str`` value that contains + only ASCII characters. If you now want to match the path info from a URL + that contains high order characters, you can pass the Unicode + representation of the decoded path portion in the pattern. + +- When using a ``traverse=`` route predicate, traversal would fail with a + URLDecodeError if there were any high-order characters in the traversal + pattern or in the matched dynamic segments. + +- Using a dynamic segment named ``traverse`` in a route pattern like this:: + + config.add_route('trav_route', 'traversal/{traverse:.*}') + + Would cause a ``UnicodeDecodeError`` when the route was matched and the + matched portion of the URL contained any high-order characters. See + https://github.com/Pylons/pyramid/issues/385 . + +- When using a ``*traverse`` stararg in a route pattern, a URL that matched + that possessed a ``@@`` in its name (signifying a view name) would be + inappropriately quoted by the traversal machinery during traversal, + resulting in the view not being found properly. See + https://github.com/Pylons/pyramid/issues/382 and + https://github.com/Pylons/pyramid/issues/375 . + +Backwards Incompatibilities +--------------------------- + +- String values passed to ``route_url`` or ``route_path`` that are meant to + replace "remainder" matches will now be URL-quoted except for embedded + slashes. For example:: + + config.add_route('remain', '/foo*remainder') + request.route_path('remain', remainder='abc / def') + # -> '/foo/abc%20/%20def' + + Previously string values passed as remainder replacements were tacked on + untouched, without any URL-quoting. But this doesn't really work logically + if the value passed is Unicode (raw unicode cannot be placed in a URL or in + a path) and it is inconsistent with the rest of the URL generation + machinery if the value is a string (it won't be quoted unless by the + caller). + + Some folks will have been relying on the older behavior to tack on query + string elements and anchor portions of the URL; sorry, you'll need to + change your code to use the ``_query`` and/or ``_anchor`` arguments to + ``route_path`` or ``route_url`` to do this now. + +- If you pass a bytestring that contains non-ASCII characters to + ``add_route`` as a pattern, it will now fail at startup time. Use Unicode + instead. + +1.3a3 (2011-12-21) +================== + +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". + +- Add undocumented ``__discriminator__`` API to derived view callables. + e.g. ``adapters.lookup(...).__discriminator__(context, request)``. It will + be used by superdynamic systems that require the discriminator to be used + for introspection after manual view lookup. + +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. + +- Removed the "Running Pyramid on Google App Engine" tutorial from the main + docs. It survives on in the Cookbook + (http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/gae.html). + Rationale: it provides the correct info for the Python 2.5 version of GAE + only, and this version of Pyramid does not support Python 2.5. + +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 +------------- + +- Added documentation to "View Configuration" narrative documentation chapter + about ``view_defaults`` class decorator. + +- 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) +================== + +Features +-------- + +- Python 3.2 compatibility. + +- New ``pyramid.compat`` module and API documentation which provides Python + 2/3 straddling support for Pyramid add-ons and development environments. + +- A ``mako.directories`` setting is no longer required to use Mako templates + Rationale: Mako template renderers can be specified using an absolute asset + spec. An entire application can be written with such asset specs, + requiring no ordered lookup path. + +- ``bpython`` interpreter compatibility in ``pshell``. See the "Command-Line + Pyramid" narrative docs chapter for more information. + +- Added ``get_appsettings`` API function to the ``pyramid.paster`` module. + This function returns the settings defined within an ``[app:...]`` section + in a PasteDeploy ini file. + +- Added ``setup_logging`` API function to the ``pyramid.paster`` module. + This function sets up Python logging according to the logging configuration + in a PasteDeploy ini file. + +- Configuration conflict reporting is reported in a more understandable way + ("Line 11 in file..." vs. a repr of a tuple of similar info). + +- A configuration introspection system was added; see the narrative + documentation chapter entitled "Pyramid Configuration Introspection" for + more information. New APIs: ``pyramid.registry.Introspectable``, + ``pyramid.config.Configurator.introspector``, + ``pyramid.config.Configurator.introspectable``, + ``pyramid.registry.Registry.introspector``. + +- Allow extra keyword arguments to be passed to the + ``pyramid.config.Configurator.action`` method. + +- New APIs: ``pyramid.path.AssetResolver`` and + ``pyramid.path.DottedNameResolver``. The former can be used to resolve + asset specifications, the latter can be used to resolve dotted names to + modules or packages. + +Bug Fixes +--------- + +- Make test suite pass on 32-bit systems; closes #286. closes #306. + See also https://github.com/Pylons/pyramid/issues/286 + +- The ``pryamid.view.view_config`` decorator did not accept a ``match_params`` + predicate argument. See https://github.com/Pylons/pyramid/pull/308 + +- The AuthTktCookieHelper could potentially generate Unicode headers + inappropriately when the ``tokens`` argument to remember was used. See + https://github.com/Pylons/pyramid/pull/314. + +- The AuthTktAuthenticationPolicy did not use a timing-attack-aware string + comparator. See https://github.com/Pylons/pyramid/pull/320 for more info. + +- The DummySession in ``pyramid.testing`` now generates a new CSRF token if + one doesn't yet exist. + +- ``request.static_url`` now generates URL-quoted URLs when fed a ``path`` + argument which contains characters that are unsuitable for URLs. See + https://github.com/Pylons/pyramid/issues/349 for more info. + +- Prevent a scaffold rendering from being named ``site`` (conflicts with + Python internal site.py). + +- Support for using instances as targets of the ``pyramid.wsgi.wsgiapp`` and + ``pryramid.wsgi.wsgiapp2`` functions. + See https://github.com/Pylons/pyramid/pull/370 for more info. + +Backwards Incompatibilities +--------------------------- + +- Pyramid no longer runs on Python 2.5 (which includes the most recent + release of Jython and the Python 2.5 version of GAE as of this writing). + +- The ``paster`` command is no longer the documented way to create projects, + start the server, or run debugging commands. To create projects from + scaffolds, ``paster create`` is replaced by the ``pcreate`` console script. + To serve up a project, ``paster serve`` is replaced by the ``pserve`` + console script. New console scripts named ``pshell``, ``pviews``, + ``proutes``, and ``ptweens`` do what their ``paster <commandname>`` + equivalents used to do. Rationale: the Paste and PasteScript packages do + not run under Python 3. + +- The default WSGI server run as the result of ``pserve`` from newly rendered + scaffolding is now the ``wsgiref`` WSGI server instead of the + ``paste.httpserver`` server. Rationale: Rationale: the Paste and + PasteScript packages do not run under Python 3. + +- The ``pshell`` command (see "paster pshell") no longer accepts a + ``--disable-ipython`` command-line argument. Instead, it accepts a ``-p`` + or ``--python-shell`` argument, which can be any of the values ``python``, + ``ipython`` or ``bpython``. + +- Removed the ``pyramid.renderers.renderer_from_name`` function. It has been + deprecated since Pyramid 1.0, and was never an API. + +- To use ZCML with versions of Pyramid >= 1.3, you will need ``pyramid_zcml`` + version >= 0.8 and ``zope.configuration`` version >= 3.8.0. The + ``pyramid_zcml`` package version 0.8 is backwards compatible all the way to + Pyramid 1.0, so you won't be warned if you have older versions installed + and upgrade Pyramid "in-place"; it may simply break instead. + +Dependencies +------------ + +- Pyramid no longer depends on the ``zope.component`` package, except as a + testing dependency. + +- Pyramid now depends on a zope.interface>=3.8.0, WebOb>=1.2dev, + repoze.lru>=0.4, zope.deprecation>=3.5.0, translationstring>=0.4 (for + Python 3 compatibility purposes). It also, as a testing dependency, + depends on WebTest>=1.3.1 for the same reason. + +- Pyramid no longer depends on the Paste or PasteScript packages. + +Documentation +------------- + +- The SQLAlchemy Wiki tutorial has been updated. It now uses + ``@view_config`` decorators and an explicit database population script. + +- Minor updates to the ZODB Wiki tutorial. + +- A narrative documentation chapter named "Extending Pyramid Configuration" + was added; it describes how to add a new directive, and how use the + ``pyramid.config.Configurator.action`` method within custom directives. It + also describes how to add introspectable objects. + +- A narrative documentation chapter named "Pyramid Configuration + Introspection" was added. It describes how to query the introspection + system. + +Scaffolds +--------- + +- Rendered scaffolds have now been changed to be more relocatable (fewer + mentions of the package name within files in the package). + +- The ``routesalchemy`` scaffold has been renamed ``alchemy``, replacing the + older (traversal-based) ``alchemy`` scaffold (which has been retired). + +- The ``starter`` scaffold now uses URL dispatch by default. + 1.2 (2011-09-12) ================ @@ -4,6 +4,11 @@ Pyramid TODOs Nice-to-Have ------------ +- Provide the presumed renderer name to the called view as an attribute of + the request. + +- Have action methods return their discriminators. + - Add docs about upgrading between Pyramid versions (e.g. how to see deprecation warnings). @@ -139,3 +144,6 @@ Probably Bad Ideas - Have ``remember`` and ``forget`` actually set headers on the response using a response callback (and return the empty list)? + +- http://pythonguy.wordpress.com/2011/06/22/dynamic-variables-revisited/ + instead of thread locals diff --git a/docs/api/renderers.rst b/docs/api/renderers.rst index 312aa0b31..ea000ad02 100644 --- a/docs/api/renderers.rst +++ b/docs/api/renderers.rst @@ -11,6 +11,8 @@ .. autofunction:: render_to_response +.. autoclass:: JSON + .. autoclass:: JSONP .. attribute:: null_renderer diff --git a/docs/conf.py b/docs/conf.py index d694b2151..db972261d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -80,7 +80,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.3b3' +version = '1.4dev' # The full version, including alpha/beta/rc tags. release = version @@ -162,7 +162,7 @@ html_theme_path = ['_themes'] html_theme = 'pyramid' html_theme_options = dict( github_url='https://github.com/Pylons/pyramid', -# in_progress='true' + in_progress='true', ) # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths @@ -476,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.3' +epub_title = 'The Pyramid Web Application Development Framework, Version 1.4' epub_author = 'Chris McDonough' epub_publisher = 'Agendaless Consulting' epub_copyright = '2008-2011' @@ -493,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.3' +epub_uid = 'The Pyramid Web Application Development Framework, Version 1.4' # 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/glossary.rst b/docs/glossary.rst index 60920a73a..88598354a 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -290,7 +290,7 @@ Glossary :term:`principal` (or principals) associated with a request. WSGI - `Web Server Gateway Interface <http://wsgi.org/>`_. This is a + `Web Server Gateway Interface <http://www.wsgi.org/>`_. This is a Python standard for connecting web applications to web servers, similar to the concept of Java Servlets. :app:`Pyramid` requires that your application be served as a WSGI application. @@ -299,7 +299,7 @@ Glossary *Middleware* is a :term:`WSGI` concept. It is a WSGI component that acts both as a server and an application. Interesting uses for middleware exist, such as caching, content-transport - encoding, and other functions. See `WSGI.org <http://wsgi.org>`_ + encoding, and other functions. See `WSGI.org <http://www.wsgi.org>`_ or `PyPI <http://python.org/pypi>`_ to find middleware for your application. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index b6e3dd163..a2143b3c5 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -145,7 +145,7 @@ the view which generates it can be overridden as necessary. The :term:`forbidden view` callable is a view callable like any other. The :term:`view configuration` which causes it to be a "forbidden" view consists -of using the meth:`pyramid.config.Configurator.add_forbidden_view` API or the +of using the :meth:`pyramid.config.Configurator.add_forbidden_view` API or the :class:`pyramid.view.forbidden_view_config` decorator. For example, you can add a forbidden view by using the @@ -171,7 +171,7 @@ as a forbidden view: from pyramid.view import forbidden_view_config - forbidden_view_config() + @forbidden_view_config() def forbidden(request): return Response('forbidden') @@ -625,7 +625,7 @@ converts the arbitrary return value into something that implements :class:`~pyramid.interfaces.IResponse`. For example, if you'd like to allow view callables to return bare string -objects (without requiring a a :term:`renderer` to convert a string to a +objects (without requiring a :term:`renderer` to convert a string to a response object), you can register an adapter which converts the string to a Response: diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 57073900f..d18d93605 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -447,7 +447,7 @@ first column instead, for example like this: pyramid.includes = #pyramid_debugtoolbar -When you attempt to restart the application with a section like the abvoe +When you attempt to restart the application with a section like the above you'll receive an error that ends something like this, and the application will not start: diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 76035cbdf..02063a112 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -177,8 +177,10 @@ using the API of the ``request.response`` attribute. See .. index:: pair: renderer; JSON -``json``: JSON Renderer -~~~~~~~~~~~~~~~~~~~~~~~ +.. _json_renderer: + +JSON Renderer +~~~~~~~~~~~~~ The ``json`` renderer renders view callable results to :term:`JSON`. It passes the return value through the ``json.dumps`` standard library function, @@ -207,7 +209,13 @@ representing the JSON serialization of the return value: '{"content": "Hello!"}' The return value needn't be a dictionary, but the return value must contain -values serializable by :func:`json.dumps`. +values serializable by ``json.dumps``. + +.. note:: + + Extra arguments can be passed to ``json.dumps`` by overriding the default + ``json`` renderer. See :class:`pyramid.renderers.JSON` and + :ref:`adding_and_overriding_renderers` for more information. You can configure a view to use the JSON renderer by naming ``json`` as the ``renderer`` argument of a view configuration, e.g. by using @@ -217,22 +225,83 @@ You can configure a view to use the JSON renderer by naming ``json`` as the :linenos: config.add_view('myproject.views.hello_world', - name='hello', - context='myproject.resources.Hello', - renderer='json') - + name='hello', + context='myproject.resources.Hello', + renderer='json') Views which use the JSON renderer can vary non-body response attributes by using the api of the ``request.response`` attribute. See :ref:`request_response_attr`. +.. _json_serializing_custom_objects: + +Serializing Custom Objects +++++++++++++++++++++++++++ + +Custom objects can be made easily JSON-serializable in Pyramid by defining a +``__json__`` method on the object's class. This method should return values +natively serializable by ``json.dumps`` (such as ints, lists, dictionaries, +strings, and so forth). + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + + class MyObject(object): + def __init__(self, x): + self.x = x + + def __json__(self): + return {'x':self.x} + + @view_config(renderer='json') + def objects(request): + return [MyObject(1), MyObject(2)] + + # the JSON value returned by ``objects`` will be: + # [{"x": 1}, {"x": 2}] + +If you aren't the author of the objects being serialized, it won't be +possible (or at least not reasonable) to add a custom ``__json__`` method to +to their classes in order to influence serialization. If the object passed +to the renderer is not a serializable type, and has no ``__json__`` method, +usually a :exc:`TypeError` will be raised during serialization. You can +change this behavior by creating a JSON renderer with a "default" function +which tries to "sniff" at the object, and returns a valid serialization (a +string) or raises a TypeError if it can't determine what to do with the +object. A short example follows: + +.. code-block:: python + :linenos: + + from pyramid.renderers import JSON + + def default(obj): + if isinstance(obj, datetime.datetime): + return obj.isoformat() + raise TypeError('%r is not serializable % (obj,)) + + json_renderer = JSON(default=default) + + # then during configuration .... + config = Configurator() + config.add_renderer('json', json_renderer) + +See :class:`pyramid.renderers.JSON` and +:ref:`adding_and_overriding_renderers` for more information. + +.. note:: + + Serializing custom objects is a feature new in Pyramid 1.4. + .. index:: pair: renderer; JSONP .. _jsonp_renderer: JSONP Renderer --------------- +~~~~~~~~~~~~~~ .. note:: This feature is new in Pyramid 1.1. @@ -297,6 +366,10 @@ The string ``callback=?`` above in the the ``url`` param to the JQuery a JSONP request; the ``callback`` parameter will be automatically filled in for you and used. +The same custom-object serialization scheme defined used for a "normal" JSON +renderer in :ref:`json_serializing_custom_objects` can be used when passing +values to a JSONP renderer too. + .. index:: pair: renderer; chameleon diff --git a/docs/narr/startup.rst b/docs/narr/startup.rst index 8e28835af..f5c741f52 100644 --- a/docs/narr/startup.rst +++ b/docs/narr/startup.rst @@ -42,8 +42,8 @@ Here's a high-level time-ordered overview of what happens when you press ``[pipeline:main]``, or ``[composite:main]`` in the ``.ini`` file. This section represents the configuration of a :term:`WSGI` application that will be served. If you're using a simple application (e.g. - ``[app:main]``), the application :term:`entry point` or :term:`dotted - Python name` will be named on the ``use=`` line within the section's + ``[app:main]``), the application's ``paste.app_factory`` :term:`entry + point` will be named on the ``use=`` line within the section's configuration. If, instead of a simple application, you're using a WSGI :term:`pipeline` (e.g. a ``[pipeline:main]`` section), the application named on the "last" element will refer to your :app:`Pyramid` application. @@ -59,11 +59,11 @@ Here's a high-level time-ordered overview of what happens when you press system for this application. See :ref:`logging_config` for more information. -#. The application's *constructor* named by the entry point reference or - dotted Python name on the ``use=`` line of the section representing your - :app:`Pyramid` application is passed the key/value parameters mentioned - within the section in which it's defined. The constructor is meant to - return a :term:`router` instance, which is a :term:`WSGI` application. +#. The application's *constructor* named by the entry point reference on the + ``use=`` line of the section representing your :app:`Pyramid` application + is passed the key/value parameters mentioned within the section in which + it's defined. The constructor is meant to return a :term:`router` + instance, which is a :term:`WSGI` application. For :app:`Pyramid` applications, the constructor will be a function named ``main`` in the ``__init__.py`` file within the :term:`package` in which diff --git a/docs/narr/templates.rst b/docs/narr/templates.rst index ed700f7b4..9db0b1c4d 100644 --- a/docs/narr/templates.rst +++ b/docs/narr/templates.rst @@ -253,16 +253,26 @@ System Values Used During Rendering When a template is rendered using :func:`~pyramid.renderers.render_to_response` or -:func:`~pyramid.renderers.render`, the renderer representing the -template will be provided with a number of *system* values. These -values are provided in a dictionary to the renderer and include: - -``context`` - The current :app:`Pyramid` context if ``request`` was provided as - a keyword argument, or ``None``. +:func:`~pyramid.renderers.render`, or a ``renderer=`` argument to view +configuration (see :ref:`templates_used_as_renderers`), the renderer +representing the template will be provided with a number of *system* values. +These values are provided to the template: ``request`` - The request provided as a keyword argument. + The value provided as the ``request`` keyword argument to + ``render_to_response`` or ``render`` *or* the request object passed to the + view when the ``renderer=`` argument to view configuration is being used to + render the template. + +``req`` + An alias for ``request``. + +``context`` + The current :app:`Pyramid` :term:`context` if ``request`` was provided as a + keyword argument to ``render_to_response`` or ``render``, or ``None`` if + the ``request`` keyword argument was not provided. This value will always + be provided if the template is rendered as the result of a ``renderer=`` + argument to view configuration being used. ``renderer_name`` The renderer name used to perform the rendering, @@ -270,17 +280,24 @@ values are provided in a dictionary to the renderer and include: ``renderer_info`` An object implementing the :class:`pyramid.interfaces.IRendererInfo` - interface. Basically, an object with the following attributes: - ``name``, ``package`` and ``type``. + interface. Basically, an object with the following attributes: ``name``, + ``package`` and ``type``. + +``view`` + The view callable object that was used to render this template. If the + view callable is a method of a class-based view, this will be an instance + of the class that the method was defined on. If the view callable is a + function or instance, it will be that function or instance. Note that this + value will only be automatically present when a template is rendered as a + result of a ``renderer=`` argument; it will be ``None`` when the + ``render_to_response`` or ``render`` APIs are used. -You can define more values which will be passed to every template -executed as a result of rendering by defining :term:`renderer -globals`. +You can define more values which will be passed to every template executed as +a result of rendering by defining :term:`renderer globals`. What any particular renderer does with these system values is up to the -renderer itself, but most template renderers, including Chameleon and -Mako renderers, make these names available as top-level template -variables. +renderer itself, but most template renderers, including Chameleon and Mako +renderers, make these names available as top-level template variables. .. index:: pair: renderer; templates diff --git a/docs/narr/views.rst b/docs/narr/views.rst index c3bbbd50e..f6ee9a8d5 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -572,6 +572,17 @@ No matter which view calling convention is used, the view code always has access to the context via ``request.context``. .. index:: + single: Passing in configuration variables + +.. _passing_in_config_variables: + +Passing Configuration Variables to a View +----------------------------------------- + +For information on passing a variable from the configuration .ini files to a +view, see :ref:`deployment_settings`. + +.. index:: single: Pylons-style controller dispatch Pylons-1.0-Style "Controller" Dispatch diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index e599e7086..9e0bf0f09 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -2,121 +2,186 @@ Adding Authorization ==================== -Our application currently allows anyone with access to the server to view, -edit, and add pages to our wiki. For purposes of demonstration we'll change -our application to allow people who are members of a *group* named -``group:editors`` to add and edit wiki pages but we'll continue allowing -anyone with access to the server to view pages. :app:`Pyramid` provides -facilities for :term:`authorization` and :term:`authentication`. We'll make -use of both features to provide security to our application. - -We will add an :term:`authentication policy` and an -:term:`authorization policy` to our :term:`application -registry`, add a ``security.py`` module and give our :term:`root` -resource an :term:`ACL`. - -Then we will add ``login`` and ``logout`` views, and modify the -existing views to make them return a ``logged_in`` flag to the -renderer and add :term:`permission` declarations to their ``view_config`` -decorators. - -Finally, we will add a ``login.pt`` template and change the existing -``view.pt`` and ``edit.pt`` to show a "Logout" link when not logged in. - -The source code for this tutorial stage can be browsed via +:app:`Pyramid` provides facilities for :term:`authentication` and +:term:`authorization`. We'll make use of both features to provide security +to our application. Our application currently allows anyone with access to +the server to view, edit, and add pages to our wiki. We'll change that +to allow only people who are members of a *group* named ``group:editors`` +to add and edit wiki pages but we'll continue allowing +anyone with access to the server to view pages. + +We will also add a login page and a logout link on all the +pages. The login page will be shown when a user is denied +access to any of the views that require a permission, instead of +a default "403 Forbidden" page. + +We will implement the access control with the following steps: + +* Add users and groups (``security.py``, a new module). +* Add an :term:`ACL` (``models.py``). +* Add an :term:`authentication policy` and an :term:`authorization policy` + (``__init__.py``). +* Add :term:`permission` declarations to the ``edit_page`` and ``add_page`` + views (``views.py``). + +Then we will add the login and logout feature: + +* Add ``login`` and ``logout`` views (``views.py``). +* Add a login template (``login.pt``). +* Make the existing views return a ``logged_in`` flag to the renderer (``views.py``). +* Add a "Logout" link to be shown when logged in and viewing or editing a page + (``view.pt``, ``edit.pt``). + +The source code for this tutorial stage can be browsed at `http://github.com/Pylons/pyramid/tree/1.3-branch/docs/tutorials/wiki/src/authorization/ <http://github.com/Pylons/pyramid/tree/1.3-branch/docs/tutorials/wiki/src/authorization/>`_. -Add Authentication and Authorization Policies -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Access Control +-------------- -We'll change our package's ``__init__.py`` file to enable an -``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` to enable -declarative security checking. We need to import the new policies: +Add users and groups +~~~~~~~~~~~~~~~~~~~~ -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 4-5,8 +Create a new ``tutorial/tutorial/security.py`` module with the +following content: + +.. literalinclude:: src/authorization/tutorial/security.py :linenos: :language: python -Then, we'll add those policies to the configuration: +The ``groupfinder`` function accepts a userid and a request and +returns one of these values: -.. literalinclude:: src/authorization/tutorial/__init__.py - :lines: 17-22 +- If the userid exists in the system, it will return a + sequence of group identifiers (or an empty sequence if the user + isn't a member of any groups). +- If the userid *does not* exist in the system, it will + return ``None``. + +For example, ``groupfinder('editor', request )`` returns ['group:editor'], +``groupfinder('viewer', request)`` returns [], and ``groupfinder('admin', request)`` +returns ``None``. We will use ``groupfinder()`` as an :term:`authentication policy` +"callback" that will provide the :term:`principal` or principals +for a user. + +In a production system, user and group +data will most often come from a database, but here we use "dummy" +data to represent user and groups sources. + +Add an ACL +~~~~~~~~~~ + +Open ``tutorial/tutorial/models.py`` and add the following import +statement at the head: + +.. literalinclude:: src/authorization/tutorial/models.py + :lines: 4-7 :linenos: :language: python -Note that the creation of an ``AuthTktAuthenticationPolicy`` requires two -arguments: ``secret`` and ``callback``. ``secret`` is a string representing -an encryption key used by the "authentication ticket" machinery represented -by this policy: it is required. The ``callback`` is a reference to a -``groupfinder`` function in the ``tutorial`` package's ``security.py`` file. -We haven't added that module yet, but we're about to. +Add the following lines to the ``Wiki`` class: + +.. literalinclude:: src/authorization/tutorial/models.py + :lines: 9-13 + :linenos: + :emphasize-lines: 4-5 + :language: python -When you're done, your ``__init__.py`` will -look like so: +We import :data:`~pyramid.security.Allow`, an action that +means that permission is allowed:, and +:data:`~pyramid.security.Everyone`, a special :term:`principal` +that is associated to all requests. Both are used in the +:term:`ACE` entries that make up the ACL. + +The ACL is a list that needs to be named `__acl__` and be an +attribute of a class. We define an :term:`ACL` with two +:term:`ACE` entries: the first entry allows any user the `view` +permission. The second entry allows the ``group:editors`` +principal the `edit` permission. + +The ``Wiki`` class that contains the ACL is the :term:`resource` +constructor for the :term:`root` resource, which is +a ``Wiki`` instance. The ACL is +provided to each view in the :term:`context` of the request, as +the ``context`` attribute. + +It's only happenstance that we're assigning this ACL at class scope. An ACL +can be attached to an object *instance* too; this is how "row level security" +can be achieved in :app:`Pyramid` applications. We actually only need *one* +ACL for the entire system, however, because our security requirements are +simple, so this feature is not demonstrated. See +:ref:`assigning_acls` for more information about what an +:term:`ACL` represents. + +Add Authentication and Authorization Policies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Open ``tutorial/__init__.py`` and +add these import statements: .. literalinclude:: src/authorization/tutorial/__init__.py + :lines: 4-5,8 :linenos: :language: python -Add ``security.py`` -~~~~~~~~~~~~~~~~~~~ - -Add a ``security.py`` module within your package (in the same -directory as ``__init__.py``, ``views.py``, etc.) with the following -content: +Now add those policies to the configuration: -.. literalinclude:: src/authorization/tutorial/security.py +.. literalinclude:: src/authorization/tutorial/__init__.py + :lines: 17-22 :linenos: + :emphasize-lines: 1-3,5-6 :language: python -The ``groupfinder`` function defined here is an :term:`authentication policy` -"callback"; it is a callable that accepts a userid and a request. If the -userid exists in the system, the callback will return a sequence of group -identifiers (or an empty sequence if the user isn't a member of any groups). -If the userid *does not* exist in the system, the callback will return -``None``. In a production system, user and group data will most often come -from a database, but here we use "dummy" data to represent user and groups -sources. Note that the ``editor`` user is a member of the ``group:editors`` -group in our dummy group data (the ``GROUPS`` data structure). - -Give Our Root Resource an ACL -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +(Only the highlighted lines need to be added.) + +We are enabling an ``AuthTktAuthenticationPolicy``, it is based in an auth +ticket that may be included in the request, and an ``ACLAuthorizationPolicy`` +that uses an ACL to determine the allow or deny outcome for a view. -We need to give our root resource object an :term:`ACL`. This ACL will be -sufficient to provide enough information to the :app:`Pyramid` security -machinery to challenge a user who doesn't have appropriate credentials when -he attempts to invoke the ``add_page`` or ``edit_page`` views. +Note that the +:class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor +accepts two arguments: ``secret`` and ``callback``. ``secret`` is a string +representing an encryption key used by the "authentication ticket" machinery +represented by this policy: it is required. The ``callback`` is the +``groupfinder()`` function that we created before. -We need to perform some imports at module scope in our ``models.py`` file: +Add permission declarations +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add a ``permission='edit'`` parameter to the ``@view_config`` +decorator for ``add_page()`` and ``edit_page()``, for example: .. code-block:: python :linenos: + :emphasize-lines: 2 + + @view_config(route_name='add_page', renderer='templates/edit.pt', + permission='edit') + +(Only the highlighted line needs to be added.) - from pyramid.security import Allow - from pyramid.security import Everyone +The result is that only users who possess the ``edit`` +permission at the time of the request may invoke those two views. -Our root resource object is a ``Wiki`` instance. We'll add the following -line at class scope to our ``Wiki`` class: +Add a ``permission='view'`` parameter to the ``@view_config`` +decorator for ``view_wiki()`` and ``view_page()``, like this: .. code-block:: python :linenos: + :emphasize-lines: 2 - __acl__ = [ (Allow, Everyone, 'view'), - (Allow, 'group:editors', 'edit') ] + @view_config(route_name='view_page', renderer='templates/view.pt', + permission='view') -It's only happenstance that we're assigning this ACL at class scope. An ACL -can be attached to an object *instance* too; this is how "row level security" -can be achieved in :app:`Pyramid` applications. We actually only need *one* -ACL for the entire system, however, because our security requirements are -simple, so this feature is not demonstrated. +(Only the highlighted line needs to be added.) -Our resulting ``models.py`` file will now look like so: +This allows anyone to invoke these two views. -.. literalinclude:: src/authorization/tutorial/models.py - :linenos: - :language: python +We are done with the changes needed to control access. The +changes that follow will add the login and logout feature. + +Login, Logout +------------- Add Login and Logout Views ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -124,124 +189,103 @@ Add Login and Logout Views We'll add a ``login`` view which renders a login form and processes the post from the login form, checking credentials. -We'll also add a ``logout`` view to our application and provide a link -to it. This view will clear the credentials of the logged in user and -redirect back to the front page. +We'll also add a ``logout`` view callable to our application and +provide a link to it. This view will clear the credentials of the +logged in user and redirect back to the front page. -We'll add these views to the existing ``views.py`` file we have in our -project. Here's what the ``login`` view callable will look like: +Add the following import statements to the +head of ``tutorial/tutorial/views.py``: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 86-113 + :lines: 6-13,15-17 :linenos: + :emphasize-lines: 3,6-9,11 :language: python -Here's what the ``logout`` view callable will look like: +(Only the highlighted lines need to be added.) + +:meth:`~pyramid.view.forbidden_view_config` will be used +to customize the default 403 Forbidden page. +:meth:`~pyramid.security.remember` and +:meth:`~pyramid.security.forget` help to create and +expire an auth ticket cookie. + +Now add the ``login`` and ``logout`` views: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 115-119 + :lines: 87-120 :linenos: :language: python -Note that the ``login`` view callable has *two* view configuration -decorators. The order of these decorators is unimportant. Each just adds a -different :term:`view configuration` for the ``login`` view callable. - -The first view configuration decorator configures the ``login`` view callable -so it will be invoked when someone visits ``/login`` (when the context is a -Wiki and the view name is ``login``). The second decorator, named -``forbidden_view_config`` specifies a :term:`forbidden view`. This -configures our login view to be presented to the user when :app:`Pyramid` -detects that a view invocation can not be authorized. Because we've -configured a forbidden view, the ``login`` view callable will be invoked -whenever one of our users tries to execute a view callable that they are not -allowed to invoke as determined by the :term:`authorization policy` in use. -In our application, for example, this means that if a user has not logged in, -and he tries to add or edit a Wiki page, he will be shown the login form. -Before being allowed to continue on to the add or edit form, he will have to -provide credentials that give him permission to add or edit via this login -form. - -Note that we're relying on some additional imports within the bodies of these -views (e.g. ``remember`` and ``forget``). We'll see a rendering of the -entire views.py file a little later here to show you where those come from. - -Change Existing Views -~~~~~~~~~~~~~~~~~~~~~ - -In order to indicate whether the current user is logged in, we need to change -each of our ``view_page``, ``edit_page`` and ``add_page`` views in -``views.py`` to pass a "logged in" parameter into its template. We'll add -something like this to each view body: +``login()`` is decorated with two decorators: -.. code-block:: python +- a ``@view_config`` decorator which associates it with the + ``login`` route and makes it visible when we visit ``/login``, +- a ``@forbidden_view_config`` decorator which turns it into + an :term:`forbidden view`. ``login()`` will be invoked + when a users tries to execute a view callable that + they are not allowed to. For example, if a user has not logged in + and tries to add or edit a Wiki page, he will be shown the + login form before being allowed to continue on. + +The order of these two :term:`view configuration` decorators +is unimportant. + +``logout()`` is decorated with a ``@view_config`` decorator +which associates it with the ``logout`` route. It will be +invoked when we visit ``/logout``. + +Add the ``login.pt`` Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create ``tutorial/tutorial/templates/login.pt`` with the following +content: + +.. literalinclude:: src/authorization/tutorial/templates/login.pt + :language: xml + +The above template is referred to within the login view we just +added to ``views.py``. + +Return a logged_in flag to the renderer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Add the following line to the import at the head of +``tutorial/tutorial/views.py``: + +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 11-15 :linenos: + :emphasize-lines: 4 + :language: python - from pyramid.security import authenticated_userid - logged_in = authenticated_userid(request) +(Only the highlighted line needs to be added.) -We'll then change the return value of each view that has an associated -``renderer`` to pass the resulting ``logged_in`` value to the -template. For example: +Add a ``logged_in`` parameter to the return value of +``view_page()``, ``edit_page()`` and ``add_page()``, +like this: .. code-block:: python :linenos: + :emphasize-lines: 4 - return dict(page = context, + return dict(page = page, content = content, - logged_in = logged_in, - edit_url = edit_url) - -Add ``permission`` Declarations to our ``view_config`` Decorators -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To protect each of our views with a particular permission, we need to pass a -``permission`` argument to each of our :class:`pyramid.view.view_config` -decorators. To do so, within ``views.py``: - -- We add ``permission='view'`` to the decorator attached to the - ``view_wiki`` and ``view_page`` view functions. This makes the - assertion that only users who possess the ``view`` permission - against the context resource at the time of the request may - invoke these views. We've granted - :data:`pyramid.security.Everyone` the view permission at the - root model via its ACL, so everyone will be able to invoke the - ``view_wiki`` and ``view_page`` views. - -- We add ``permission='edit'`` to the decorator attached to the - ``add_page`` and ``edit_page`` view functions. This makes the - assertion that only users who possess the effective ``edit`` - permission against the context resource at the time of the - request may invoke these views. We've granted the - ``group:editors`` principal the ``edit`` permission at the - root model via its ACL, so only a user whom is a member of - the group named ``group:editors`` will able to invoke the - ``add_page`` or ``edit_page`` views. We've likewise given - the ``editor`` user membership to this group via the - ``security.py`` file by mapping him to the ``group:editors`` - group in the ``GROUPS`` data structure (``GROUPS - = {'editor':['group:editors']}``); the ``groupfinder`` - function consults the ``GROUPS`` data structure. This means - that the ``editor`` user can add and edit pages. + edit_url = edit_url, + logged_in = authenticated_userid(request)) -Add the ``login.pt`` Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Add a ``login.pt`` template to your templates directory. It's -referred to within the login view we just added to ``views.py``. +(Only the highlighted line needs to be added.) -.. literalinclude:: src/authorization/tutorial/templates/login.pt - :language: xml +:meth:`~pyramid.security.authenticated_userid()` will return None +if the user is not authenticated, or some user id it the user +is authenticated. -Change ``view.pt`` and ``edit.pt`` +Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -We'll also need to change our ``edit.pt`` and ``view.pt`` templates to -display a "Logout" link if someone is logged in. This link will -invoke the logout view. - -To do so we'll add this to both templates within the ``<div id="right" -class="app-welcome align-right">`` div: +Open ``tutorial/tutorial/templates/edit.pt`` and +``tutorial/tutorial/templates/view.pt`` and add this within the +``<div id="right" class="app-welcome align-right">`` div: .. code-block:: xml @@ -249,57 +293,96 @@ class="app-welcome align-right">`` div: <a href="${request.application_url}/logout">Logout</a> </span> -See Our Changes To ``views.py`` and our Templates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The attribute ``tal:condition="logged_in"`` will make the element be +included when ``logged_in`` is any user id. The link will invoke +the logout view. The above element will not be included if ``logged_in`` +is ``None``, such as when a user is not authenticated. + +Seeing Our Changes +------------------ + +Our ``tutorial/tutorial/__init__.py`` will look something like this +when we're done: + +.. literalinclude:: src/authorization/tutorial/__init__.py + :linenos: + :emphasize-lines: 4-5,8,17-19,21-22 + :language: python + +(Only the highlighted lines need to be added.) + +Our ``tutorial/tutorial/models.py`` will look something like this +when we're done: + +.. literalinclude:: src/authorization/tutorial/models.py + :linenos: + :emphasize-lines: 4-7,12-13 + :language: python -Our ``views.py`` module will look something like this when we're done: +(Only the highlighted lines need to be added.) + +Our ``tutorial/tutorial/views.py`` will look something like this +when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: + :emphasize-lines: 8,11-15,17,24,29,48,52,68,72,80,82-120 :language: python -Our ``edit.pt`` template will look something like this when we're done: +(Only the highlighted lines need to be added.) + +Our ``tutorial/tutorial/templates/edit.pt`` template will look +something like this when we're done: .. literalinclude:: src/authorization/tutorial/templates/edit.pt :linenos: + :emphasize-lines: 41-43 :language: xml -Our ``view.pt`` template will look something like this when we're done: +(Only the highlighted lines need to be added.) + +Our ``tutorial/tutorial/templates/view.pt`` template will look +something like this when we're done: .. literalinclude:: src/authorization/tutorial/templates/view.pt :linenos: + :emphasize-lines: 41-43 :language: xml -View the Application in a Browser -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +(Only the highlighted lines need to be added.) -We can finally examine our application in a browser. The views we'll try are -as follows: +Viewing the Application in a Browser +------------------------------------ -- Visiting ``http://localhost:6543/`` in a browser invokes the ``view_wiki`` - view. This always redirects to the ``view_page`` view of the ``FrontPage`` - page resource. It is executable by any user. +We can finally examine our application in a browser (See +:ref:`wiki-start-the-application`). Launch a browser and visit +each of the following URLs, check that the result is as expected: -- Visiting ``http://localhost:6543/FrontPage/`` in a browser invokes the - ``view_page`` view of the ``FrontPage`` Page resource. This is because +- ``http://localhost:6543/`` invokes the + ``view_wiki`` view. This always redirects to the ``view_page`` view + of the ``FrontPage`` Page resource. It is executable by any user. + +- ``http://localhost:6543/FrontPage`` invokes + the ``view_page`` view of the ``FrontPage`` Page resource. This is because it's the :term:`default view` (a view without a ``name``) for ``Page`` resources. It is executable by any user. -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser invokes - the edit view for the ``FrontPage`` Page resource. It is executable by - only the ``editor`` user. If a different user (or the anonymous user) - invokes it, a login form will be displayed. Supplying the credentials with - the username ``editor``, password ``editor`` will show the edit page form - being displayed. +- ``http://localhost:6543/FrontPage/edit_page`` + invokes the edit view for the FrontPage object. It is executable by + only the ``editor`` user. If a different user (or the anonymous + user) invokes it, a login form will be displayed. Supplying the + credentials with the username ``editor``, password ``editor`` will + display the edit page form. -- Visiting ``http://localhost:6543/add_page/SomePageName`` in a - browser invokes the add view for a page. It is executable by only +- ``http://localhost:6543/add_page/SomePageName`` + invokes the add view for a page. It is executable by only the ``editor`` user. If a different user (or the anonymous user) invokes it, a login form will be displayed. Supplying the credentials with the username ``editor``, password ``editor`` will - show the edit page form being displayed. + display the edit page form. -- After logging in (as a result of hitting an edit or add page and - submitting the login form with the ``editor`` credentials), we'll see - a Logout link in the upper right hand corner. When we click it, - we're logged out, and redirected back to the front page. +- After logging in (as a result of hitting an edit or add page + and submitting the login form with the ``editor`` + credentials), we'll see a Logout link in the upper right hand + corner. When we click it, we're logged out, and redirected + back to the front page. diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 12bfa8b84..28cecb787 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -229,60 +229,63 @@ this: Adding Templates ================ -Most view callables we've added expected to be rendered via a -:term:`template`. The default templating systems in :app:`Pyramid` are -:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of :term:`ZPT`, -which is an XML-based templating language. Mako is a non-XML-based -templating language. Because we had to pick one, we chose Chameleon for this -tutorial. - -The templates we create will live in the ``templates`` directory of our +The ``view_page``, ``add_page`` and ``edit_page`` views that we've added +reference a :term:`template`. Each template is a :term:`Chameleon` :term:`ZPT` +template. These templates will live in the ``templates`` directory of our tutorial package. Chameleon templates must have a ``.pt`` extension to be recognized as such. The ``view.pt`` Template ------------------------ -The ``view.pt`` template is used for viewing a single Page. It is used by -the ``view_page`` view function. It should have a div that is "structure -replaced" with the ``content`` value provided by the view. It should also -have a link on the rendered page that points at the "edit" URL (the URL which -invokes the ``edit_page`` view for the page being viewed). - -Once we're done with the ``view.pt`` template, it will look a lot like -the below: +Create ``tutorial/tutorial/templates/view.pt`` and add the following +content: .. literalinclude:: src/views/tutorial/templates/view.pt + :linenos: :language: xml -.. note:: +This template is used by ``view_page()`` for displaying a single +wiki page. It includes: - The names available for our use in a template are always those that - are present in the dictionary returned by the view callable. But our - templates make use of a ``request`` object that none of our tutorial views - return in their dictionary. This value appears as if "by magic". - However, ``request`` is one of several names that are available "by - default" in a template when a template renderer is used. See - :ref:`chameleon_template_renderers` for more information about other names - that are available by default in a template when a template is used as a - renderer. +- A ``div`` element that is replaced with the ``content`` + value provided by the view (rows 45-47). ``content`` + contains HTML, so the ``structure`` keyword is used + to prevent escaping it (i.e. changing ">" to >, etc.) +- A link that points + at the "edit" URL which invokes the ``edit_page`` view for + the page being viewed (rows 49-51). The ``edit.pt`` Template ------------------------ -The ``edit.pt`` template is used for adding and editing a Page. It is used -by the ``add_page`` and ``edit_page`` view functions. It should display a -page containing a form that POSTs back to the "save_url" argument supplied by -the view. The form should have a "body" textarea field (the page data), and -a submit button that has the name "form.submitted". The textarea in the form -should be filled with any existing page data when it is rendered. - -Once we're done with the ``edit.pt`` template, it will look a lot like the -below: +Create ``tutorial/tutorial/templates/edit.pt`` and add the following +content: .. literalinclude:: src/views/tutorial/templates/edit.pt + :linenos: :language: xml +This template is used by ``add_page()`` and ``edit_page()`` for adding +and editing a wiki page. It displays +a page containing a form that includes: + +- A 10 row by 60 column ``textarea`` field named ``body`` that is filled + with any existing page data when it is rendered (rows 46-47). +- A submit button that has the name ``form.submitted`` (row 48). + +The form POSTs back to the "save_url" argument supplied +by the view (row 45). The view will use the ``body`` and +``form.submitted`` values. + +.. note:: Our templates use a ``request`` object that + none of our tutorial views return in their dictionary. + ``request`` is one of several + names that are available "by default" in a template when a template + renderer is used. See :ref:`chameleon_template_renderers` for + information about other names that are available by default + when a Chameleon template is used as a renderer. + Static Assets ------------- @@ -302,24 +305,25 @@ Viewing the Application in a Browser ==================================== We can finally examine our application in a browser (See -:ref:`wiki-start-the-application`). The views we'll try are as follows: +:ref:`wiki-start-the-application`). Launch a browser and visit +each of the following URLs, check that the result is as expected: -- Visiting ``http://localhost:6543/`` in a browser invokes the ``view_wiki`` +- ``http://localhost:6543/`` invokes the ``view_wiki`` view. This always redirects to the ``view_page`` view of the ``FrontPage`` Page resource. -- Visiting ``http://localhost:6543/FrontPage/`` in a browser invokes +- ``http://localhost:6543/FrontPage/`` invokes the ``view_page`` view of the front page resource. This is - because it's the *default view* (a view without a ``name``) for Page + because it's the :term:`default view` (a view without a ``name``) for Page resources. -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser +- ``http://localhost:6543/FrontPage/edit_page`` invokes the edit view for the ``FrontPage`` Page resource. -- Visiting ``http://localhost:6543/add_page/SomePageName`` in a - browser invokes the add view for a Page. +- ``http://localhost:6543/add_page/SomePageName`` + invokes the add view for a Page. - To generate an error, visit ``http://localhost:6543/add_page`` which - will generate an ``IndexError`` for the expression - ``request.subpath[0]``. You'll see an interactive traceback + will generate an ``IndexErrorr: tuple index out of range`` error. + You'll see an interactive traceback facility provided by :term:`pyramid_debugtoolbar`. diff --git a/docs/tutorials/wiki/design.rst b/docs/tutorials/wiki/design.rst index 2b613377a..c94612fb1 100644 --- a/docs/tutorials/wiki/design.rst +++ b/docs/tutorials/wiki/design.rst @@ -36,9 +36,16 @@ be used as the wiki home page. Views ----- -There will be four views to handle the normal operations of -viewing, editing and adding wiki pages. Two additional views -will handle the login and logout tasks related to security. +There will be three views to handle the normal operations of adding, +editing and viewing wiki pages, plus one view for the wiki front page. +Two templates will be used, one for viewing, and one for both for adding +and editing wiki pages. + +The default templating systems in :app:`Pyramid` are +:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of +:term:`ZPT`, which is an XML-based templating language. Mako is a +non-XML-based templating language. Because we had to pick one, +we chose Chameleon for this tutorial. Security -------- @@ -52,11 +59,11 @@ use to do this are below. - GROUPS, a dictionary mapping user names to a list of groups they belong to. -- *groupfinder*, an *authorization callback* that looks up +- ``groupfinder``, an *authorization callback* that looks up USERS and GROUPS. It will be provided in a new *security.py* file. -- An :term:`ACL` is attached to the root resource. Each +- An :term:`ACL` is attached to the root :term:`resource`. Each row below details an :term:`ACE`: +----------+----------------+----------------+ @@ -70,6 +77,8 @@ use to do this are below. - Permission declarations are added to the views to assert the security policies as each request is handled. +Two additional views and one template will handle the login and +logout tasks. Summary ------- diff --git a/docs/tutorials/wiki/src/authorization/tutorial/models.py b/docs/tutorials/wiki/src/authorization/tutorial/models.py index 0a31c38be..582ff0d7e 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/models.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/models.py @@ -1,8 +1,10 @@ from persistent import Persistent from persistent.mapping import PersistentMapping -from pyramid.security import Allow -from pyramid.security import Everyone +from pyramid.security import ( + Allow, + Everyone, + ) class Wiki(PersistentMapping): __name__ = None diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index fcbe6fe25..21f12b31d 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -9,9 +9,9 @@ from pyramid.view import ( ) from pyramid.security import ( - authenticated_userid, remember, forget, + authenticated_userid, ) from .security import USERS @@ -20,12 +20,13 @@ from .models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(context='.models.Wiki', permission='view') +@view_config(context='.models.Wiki', + permission='view') def view_wiki(context, request): return HTTPFound(location=request.resource_url(context, 'FrontPage')) -@view_config(context='.models.Page', - renderer='templates/view.pt', permission='view') +@view_config(context='.models.Page', renderer='templates/view.pt', + permission='view') def view_page(context, request): wiki = context.__parent__ @@ -43,10 +44,8 @@ def view_page(context, request): content = wikiwords.sub(check, content) edit_url = request.resource_url(context, 'edit_page') - logged_in = authenticated_userid(request) - return dict(page = context, content = content, edit_url = edit_url, - logged_in = logged_in) + logged_in = authenticated_userid(request)) @view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt', @@ -65,9 +64,8 @@ def add_page(context, request): page.__name__ = name page.__parent__ = context - logged_in = authenticated_userid(request) - - return dict(page = page, save_url = save_url, logged_in = logged_in) + return dict(page = page, save_url = save_url, + logged_in = authenticated_userid(request)) @view_config(name='edit_page', context='.models.Page', renderer='templates/edit.pt', @@ -77,11 +75,9 @@ def edit_page(context, request): context.data = request.params['body'] return HTTPFound(location = request.resource_url(context)) - logged_in = authenticated_userid(request) - return dict(page = context, save_url = request.resource_url(context, 'edit_page'), - logged_in = logged_in) + logged_in = authenticated_userid(request)) @view_config(context='.models.Wiki', name='login', renderer='templates/login.pt') diff --git a/docs/tutorials/wiki/src/tests/tutorial/models.py b/docs/tutorials/wiki/src/tests/tutorial/models.py index 0a31c38be..582ff0d7e 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/models.py +++ b/docs/tutorials/wiki/src/tests/tutorial/models.py @@ -1,8 +1,10 @@ from persistent import Persistent from persistent.mapping import PersistentMapping -from pyramid.security import Allow -from pyramid.security import Everyone +from pyramid.security import ( + Allow, + Everyone, + ) class Wiki(PersistentMapping): __name__ = None diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index fcbe6fe25..21f12b31d 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -9,9 +9,9 @@ from pyramid.view import ( ) from pyramid.security import ( - authenticated_userid, remember, forget, + authenticated_userid, ) from .security import USERS @@ -20,12 +20,13 @@ from .models import Page # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(context='.models.Wiki', permission='view') +@view_config(context='.models.Wiki', + permission='view') def view_wiki(context, request): return HTTPFound(location=request.resource_url(context, 'FrontPage')) -@view_config(context='.models.Page', - renderer='templates/view.pt', permission='view') +@view_config(context='.models.Page', renderer='templates/view.pt', + permission='view') def view_page(context, request): wiki = context.__parent__ @@ -43,10 +44,8 @@ def view_page(context, request): content = wikiwords.sub(check, content) edit_url = request.resource_url(context, 'edit_page') - logged_in = authenticated_userid(request) - return dict(page = context, content = content, edit_url = edit_url, - logged_in = logged_in) + logged_in = authenticated_userid(request)) @view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt', @@ -65,9 +64,8 @@ def add_page(context, request): page.__name__ = name page.__parent__ = context - logged_in = authenticated_userid(request) - - return dict(page = page, save_url = save_url, logged_in = logged_in) + return dict(page = page, save_url = save_url, + logged_in = authenticated_userid(request)) @view_config(name='edit_page', context='.models.Page', renderer='templates/edit.pt', @@ -77,11 +75,9 @@ def edit_page(context, request): context.data = request.params['body'] return HTTPFound(location = request.resource_url(context)) - logged_in = authenticated_userid(request) - return dict(page = context, save_url = request.resource_url(context, 'edit_page'), - logged_in = logged_in) + logged_in = authenticated_userid(request)) @view_config(context='.models.Wiki', name='login', renderer='templates/login.pt') diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index 8c1c50ff6..2ef55d15b 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -8,21 +8,31 @@ Adding Authorization :term:`authorization`. We'll make use of both features to provide security to our application. Our application currently allows anyone with access to the server to view, edit, and add pages to our wiki. We'll change that -to allow only people who possess a specific username (`editor`) -to add and edit wiki pages but we'll continue allowing anyone with access to -the server to view pages. +to allow only people who are members of a *group* named ``group:editors`` +to add and edit wiki pages but we'll continue allowing +anyone with access to the server to view pages. -We will do the following steps: +We will also add a login page and a logout link on all the +pages. The login page will be shown when a user is denied +access to any of the views that require a permission, instead of +a default "403 Forbidden" page. -* Add a :term:`root factory` with an :term:`ACL` (``models.py``). +We will implement the access control with the following steps: + +* Add users and groups (``security.py``, a new module). +* Add an :term:`ACL` (``models.py`` and + ``__init__.py``). * Add an :term:`authentication policy` and an :term:`authorization policy` (``__init__.py``). -* Add an authentication policy callback (new ``security.py`` module). -* Add ``login`` and ``logout`` views (``views.py``). * Add :term:`permission` declarations to the ``edit_page`` and ``add_page`` views (``views.py``). + +Then we will add the login and logout feature: + +* Add routes for /login and /logout (``__init__.py``). +* Add ``login`` and ``logout`` views (``views.py``). +* Add a login template (``login.pt``). * Make the existing views return a ``logged_in`` flag to the renderer (``views.py``). -* Add a login template (new ``login.pt``). * Add a "Logout" link to be shown when logged in and viewing or editing a page (``view.pt``, ``edit.pt``). @@ -30,39 +40,88 @@ The source code for this tutorial stage can be browsed at `http://github.com/Pylons/pyramid/tree/1.3-branch/docs/tutorials/wiki2/src/authorization/ <http://github.com/Pylons/pyramid/tree/1.3-branch/docs/tutorials/wiki2/src/authorization/>`_. -Adding A Root Factory -~~~~~~~~~~~~~~~~~~~~~ +Access Control +-------------- + +Add users and groups +~~~~~~~~~~~~~~~~~~~~ + +Create a new ``tutorial/tutorial/security.py`` module with the +following content: + +.. literalinclude:: src/authorization/tutorial/security.py + :linenos: + :language: python + +The ``groupfinder`` function accepts a userid and a request and +returns one of these values: + +- If the userid exists in the system, it will return a + sequence of group identifiers (or an empty sequence if the user + isn't a member of any groups). +- If the userid *does not* exist in the system, it will + return ``None``. -Open ``models.py`` and add the following statements: +For example, ``groupfinder('editor', request )`` returns ['group:editor'], +``groupfinder('viewer', request)`` returns [], and ``groupfinder('admin', request)`` +returns ``None``. We will use ``groupfinder()`` as an :term:`authentication policy` +"callback" that will provide the :term:`principal` or principals +for a user. + +In a production system, user and group +data will most often come from a database, but here we use "dummy" +data to represent user and groups sources. + +Add an ACL +~~~~~~~~~~ + +Open ``tutorial/tutorial/models.py`` and add the following import +statement at the head: .. literalinclude:: src/authorization/tutorial/models.py - :lines: 1-4,35-39 + :lines: 1-4 :linenos: :language: python -We're going to start to use a custom :term:`root factory` within our -``__init__.py`` file. The objects generated by the root factory will -be used as the :term:`context` of each request to our application. -Those context objects will be decorated with security -declarations. When we use a custom root factory to generate -our contexts, we can begin to make use of the declarative security -features of :app:`Pyramid`. - -We'll modify our ``__init__.py``, passing in a :term:`root factory` to our -:term:`Configurator` constructor. We'll point it at the new class we created -inside our ``models.py`` file. - -The ``RootFactory`` class we've just added will be used by :app:`Pyramid` to -construct a ``context`` object. The context is attached to the request -object passed to our view callables as the ``context`` attribute. - -The context object generated by our root factory will possess an ``__acl__`` -attribute that allows :data:`pyramid.security.Everyone` (a special principal) -to view all pages, while allowing only a :term:`principal` named -``group:editors`` to edit and add pages. The ``__acl__`` attribute attached -to a context is interpreted specially by :app:`Pyramid` as an access control -list during view callable execution. See :ref:`assigning_acls` for more -information about what an :term:`ACL` represents. +Add the following class definition: + +.. literalinclude:: src/authorization/tutorial/models.py + :lines: 35-39 + :linenos: + :language: python + +We import :data:`~pyramid.security.Allow`, an action that +means that permission is allowed:, and +:data:`~pyramid.security.Everyone`, a special :term:`principal` +that is associated to all requests. Both are used in the +:term:`ACE` entries that make up the ACL. + +The ACL is a list that needs to be named `__acl__` and be an +attribute of a class. We define an :term:`ACL` with two +:term:`ACE` entries: the first entry allows any user the `view` +permission. The second entry allows the ``group:editors`` +principal the `edit` permission. + +The ``RootFactory`` class that contains the ACL is a :term:`root factory`. +We need to associate it to our :app:`Pyramid` application, so the ACL is +provided to each view in the :term:`context` of the request, as +the ``context`` attribute. + +Open ``tutorial/tutorial/__init__.py`` and add a ``root_factory`` +parameter to our :term:`Configurator` constructor, that points to +the class we created above: + +.. literalinclude:: src/authorization/tutorial/__init__.py + :lines: 19-20 + :linenos: + :emphasize-lines: 2 + :language: python + +(Only the highlighted line needs to be added.) + +We are now providing the ACL to the application. See +:ref:`assigning_acls` for more information about what an +:term:`ACL` represents. .. note:: @@ -71,23 +130,10 @@ information about what an :term:`ACL` represents. the ``factory`` argument to :meth:`pyramid.config.Configurator.add_route` for more info. -We'll pass the ``RootFactory`` we created in the step above in as the -``root_factory`` argument to a :term:`Configurator`. - -Add an Authorization Policy and an Authentication Policy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -We're going to be making several changes to our ``__init__.py`` file which -will help us configure an authorization policy. +Add Authentication and Authorization Policies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For any :app:`Pyramid` application to perform authorization, we need to add a -``security.py`` module (we'll do that shortly) and we'll need to change our -``__init__.py`` file to add an :term:`authentication policy` and an -:term:`authorization policy` which uses the ``security.py`` file for a -*callback*. - -We'll enable an ``AuthTktAuthenticationPolicy`` and an ``ACLAuthorizationPolicy`` -to implement declarative security checking. Open ``tutorial/__init__.py`` and +Open ``tutorial/__init__.py`` and add these import statements: .. literalinclude:: src/authorization/tutorial/__init__.py @@ -100,183 +146,173 @@ Now add those policies to the configuration: .. literalinclude:: src/authorization/tutorial/__init__.py :lines: 16-22 :linenos: + :emphasize-lines: 1-3,6-7 :language: python +(Only the highlighted lines need to be added.) + +We are enabling an ``AuthTktAuthenticationPolicy``, it is based in an auth +ticket that may be included in the request, and an ``ACLAuthorizationPolicy`` +that uses an ACL to determine the allow or deny outcome for a view. + Note that the :class:`pyramid.authentication.AuthTktAuthenticationPolicy` constructor accepts two arguments: ``secret`` and ``callback``. ``secret`` is a string representing an encryption key used by the "authentication ticket" machinery -represented by this policy: it is required. The ``callback`` is a -``groupfinder`` function in the current directory's ``security.py`` file. We -haven't added that module yet, but we're about to. +represented by this policy: it is required. The ``callback`` is the +``groupfinder()`` function that we created before. -Viewing Your Changes --------------------- +Add permission declarations +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When we're done configuring a root factory, adding a authentication and -authorization policies, and adding routes for ``/login`` and ``/logout``, -your application's ``__init__.py`` will look like this: +Add a ``permission='edit'`` parameter to the ``@view_config`` +decorator for ``add_page()`` and ``edit_page()``, for example: -.. literalinclude:: src/authorization/tutorial/__init__.py +.. code-block:: python :linenos: - :emphasize-lines: 2-3,7,16-18,20-22,25-26 - :language: python + :emphasize-lines: 2 -Adding an authentication policy callback -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + @view_config(route_name='add_page', renderer='templates/edit.pt', + permission='edit') -Add a ``tutorial/security.py`` module within your package (in the same -directory as :file:`__init__.py`, :file:`views.py`, etc.) with the -following content: +(Only the highlighted line needs to be added.) -.. literalinclude:: src/authorization/tutorial/security.py +The result is that only users who possess the ``edit`` +permission at the time of the request may invoke those two views. + +Add a ``permission='view'`` parameter to the ``@view_config`` +decorator for ``view_wiki()`` and ``view_page()``, like this: + +.. code-block:: python + :linenos: + :emphasize-lines: 2 + + @view_config(route_name='view_page', renderer='templates/view.pt', + permission='view') + +(Only the highlighted line needs to be added.) + +This allows anyone to invoke these two views. + +We are done with the changes needed to control access. The +changes that follow will add the login and logout feature. + +Login, Logout +------------- + +Add routes for /login and /logout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Go back to ``tutorial/tutorial/__init__.py`` and add these two +routes: + +.. literalinclude:: src/authorization/tutorial/__init__.py + :lines: 25-26 :linenos: :language: python -The ``groupfinder`` function defined here is an :term:`authentication policy` -"callback"; it is a callable that accepts a userid and a request. If -the userid exists in the system, the callback will return a sequence -of group identifiers (or an empty sequence if the user isn't a member -of any groups). If the userid *does not* exist in the system, the -callback will return ``None``. In a production system, user and group -data will most often come from a database, but here we use "dummy" -data to represent user and groups sources. Note that the ``editor`` -user is a member of the ``group:editors`` group in our dummy group -data (the ``GROUPS`` data structure). - -We've given the ``editor`` user membership to the ``group:editors`` by -mapping him to this group in the ``GROUPS`` data structure (``GROUPS = -{'editor':['group:editors']}``). Since the ``groupfinder`` function -consults the ``GROUPS`` data structure, this will mean that, as a -result of the ACL attached to the :term:`context` object returned by -the root factory, and the permission associated with the ``add_page`` -and ``edit_page`` views, the ``editor`` user should be able to add and -edit pages. - -Adding Login and Logout Views -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add Login and Logout Views +~~~~~~~~~~~~~~~~~~~~~~~~~~ -To our ``views.py`` we'll add a ``login`` view callable which renders a login -form and processes the post from the login form, checking credentials. +We'll add a ``login`` view which renders a login form and processes +the post from the login form, checking credentials. We'll also add a ``logout`` view callable to our application and provide a link to it. This view will clear the credentials of the logged in user and redirect back to the front page. -The ``login`` view callable will look something like this: +Add the following import statements to the +head of ``tutorial/tutorial/views.py``: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 89-115 + :lines: 9-16,18,24-25 :linenos: + :emphasize-lines: 3,6-9,11 :language: python -The ``logout`` view callable will look something like this: +(Only the highlighted lines need to be added.) -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 117-121 - :linenos: - :language: python +:meth:`~pyramid.view.forbidden_view_config` will be used +to customize the default 403 Forbidden page. +:meth:`~pyramid.security.remember` and +:meth:`~pyramid.security.forget` help to create and +expire an auth ticket cookie. -The ``login`` view callable is decorated with two decorators, a -``@view_config`` decorator, which associates it with the ``login`` -route, and a ``@forbidden_view_config`` decorator which turns it in to -an :term:`exception view`. The one which associates it with the -``login`` route makes it visible when we visit ``/login``. The other -one makes it a :term:`forbidden view`. The forbidden view is -displayed whenever Pyramid or your application raises an -:class:`pyramid.httpexceptions.HTTPForbidden` exception. In this -case, we'll be relying on the forbidden view to show the login form -whenver someone attempts to execute an action which they're not yet -authorized to perform. - -The ``logout`` view callable is decorated with a ``@view_config`` decorator -which associates it with the ``logout`` route. This makes it visible when we -visit ``/logout``. - -We'll need to import some stuff to service the needs of these two functions: -the ``pyramid.view.forbidden_view_config`` class, a number of values from the -``pyramid.security`` module, and a value from our newly added -``tutorial.security`` package. Add the following import statements to the -head of the ``views.py`` file: +Now add the ``login`` and ``logout`` views: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 9-18,24-25 + :lines: 91-123 :linenos: :language: python -Changing Existing Views -~~~~~~~~~~~~~~~~~~~~~~~ +``login()`` is decorated with two decorators: -Add permision declarations --------------------------- +- a ``@view_config`` decorator which associates it with the + ``login`` route and makes it visible when we visit ``/login``, +- a ``@forbidden_view_config`` decorator which turns it into + an :term:`forbidden view`. ``login()`` will be invoked + when a users tries to execute a view callable that + they are not allowed to. For example, if a user has not logged in + and tries to add or edit a Wiki page, he will be shown the + login form before being allowed to continue on. -Then we need to change each of our ``view_page``, ``edit_page`` and -``add_page`` view callables in ``views.py``. Within each of these views, -we'll need to pass a "logged in" parameter to its template. We'll add -something like this to each view body: +The order of these two :term:`view configuration` decorators +is unimportant. -.. code-block:: python - :linenos: +``logout()`` is decorated with a ``@view_config`` decorator +which associates it with the ``logout`` route. It will be +invoked when we visit ``/logout``. - from pyramid.security import authenticated_userid - logged_in = authenticated_userid(request) +Add the ``login.pt`` Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Return a logged_in flag to the renderer ---------------------------------------- +Create ``tutorial/tutorial/templates/login.pt`` with the following +content: -We'll then change the return value of these views to pass the resulting -``logged_in`` value to the template, e.g.: +.. literalinclude:: src/authorization/tutorial/templates/login.pt + :language: xml -.. code-block:: python - :linenos: +The above template is referred to within the login view we just +added to ``views.py``. - return dict(page = page, - content = content, - logged_in = logged_in, - edit_url = edit_url) +Return a logged_in flag to the renderer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -We'll also need to add a ``permission`` value to the ``@view_config`` -decorator for each of the ``add_page`` and ``edit_page`` view callables. For -each, we'll add ``permission='edit'``, for example: +Add the following line to the import at the head of +``tutorial/tutorial/views.py``: -.. code-block:: python +.. literalinclude:: src/authorization/tutorial/views.py + :lines: 14-18 :linenos: + :emphasize-lines: 4 + :language: python - @view_config(route_name='edit_page', renderer='templates/edit.pt', - permission='edit') +(Only the highlighted line needs to be added.) -See the ``permission='edit'`` we added there? This indicates that the view -callables which these views reference cannot be invoked without the -authenticated user possessing the ``edit`` permission with respect to the -current :term:`context`. +Add a ``logged_in`` parameter to the return value of +``view_page()``, ``edit_page()`` and ``add_page()``, +like this: -Adding these ``permission`` arguments causes Pyramid to make the -assertion that only users who possess the effective ``edit`` -permission at the time of the request may invoke those two views. -We've granted the ``group:editors`` :term:`principal` the ``edit`` -permission in the :term:`root factory` via its ACL, so only a user who -is a member of the group named ``group:editors`` will be able to -invoke the views associated with the ``add_page`` or ``edit_page`` -routes. +.. code-block:: python + :linenos: + :emphasize-lines: 4 -Adding the ``login.pt`` Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + return dict(page = page, + content = content, + edit_url = edit_url, + logged_in = authenticated_userid(request)) -Add a ``login.pt`` template to your templates directory. It's -referred to within the login view we just added to ``views.py``. +(Only the highlighted line needs to be added.) -.. literalinclude:: src/authorization/tutorial/templates/login.pt - :language: xml +:meth:`~pyramid.security.authenticated_userid()` will return None +if the user is not authenticated, or some user id it the user +is authenticated. Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -We'll also need to change our ``edit.pt`` and ``view.pt`` templates to -display a "Logout" link if someone is logged in. This link will -invoke the logout view. - -To do so we'll add this to both templates within the ``<div id="right" -class="app-welcome align-right">`` div: +Open ``tutorial/tutorial/templates/edit.pt`` and +``tutorial/tutorial/templates/view.pt`` and add this within the +``<div id="right" class="app-welcome align-right">`` div: .. code-block:: xml @@ -284,36 +320,66 @@ class="app-welcome align-right">`` div: <a href="${request.application_url}/logout">Logout</a> </span> -Seeing Our Changes To ``views.py`` and our Templates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The attribute ``tal:condition="logged_in"`` will make the element be +included when ``logged_in`` is any user id. The link will invoke +the logout view. The above element will not be included if ``logged_in`` +is ``None``, such as when a user is not authenticated. + +Seeing Our Changes +------------------ + +Our ``tutorial/tutorial/__init__.py`` will look something like this +when we're done: + +.. literalinclude:: src/authorization/tutorial/__init__.py + :linenos: + :emphasize-lines: 2-3,7,16-18,20-22,25-26 + :language: python + +(Only the highlighted lines need to be added.) -Our ``views.py`` module will look something like this when we're done: +Our ``tutorial/tutorial/models.py`` will look something like this +when we're done: + +.. literalinclude:: src/authorization/tutorial/models.py + :linenos: + :emphasize-lines: 1-4,35-39 + :language: python + +(Only the highlighted lines need to be added.) + +Our ``tutorial/tutorial/views.py`` will look something like this +when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: - :emphasize-lines: 11,14-18,56,59,71,74,89-115,117-121 + :emphasize-lines: 11,14-18,31,37,58,61,73,76,88,91-117,119-123 :language: python (Only the highlighted lines need to be added.) -Our ``edit.pt`` template will look something like this when we're done: +Our ``tutorial/tutorial/templates/edit.pt`` template will look +something like this when we're done: .. literalinclude:: src/authorization/tutorial/templates/edit.pt + :linenos: :emphasize-lines: 41-43 :language: xml (Only the highlighted lines need to be added.) -Our ``view.pt`` template will look something like this when we're done: +Our ``tutorial/tutorial/templates/view.pt`` template will look +something like this when we're done: .. literalinclude:: src/authorization/tutorial/templates/view.pt + :linenos: :emphasize-lines: 41-43 :language: xml (Only the highlighted lines need to be added.) Viewing the Application in a Browser -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------ We can finally examine our application in a browser (See :ref:`wiki2-start-the-application`). Launch a browser and visit diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst index ac58e1e46..efb72230e 100644 --- a/docs/tutorials/wiki2/definingviews.rst +++ b/docs/tutorials/wiki2/definingviews.rst @@ -226,52 +226,63 @@ of the wiki page. Adding Templates ================ -The views we've added all reference a :term:`template`. Each template is a -:term:`Chameleon` :term:`ZPT` template. These templates will live in the -``templates`` directory of our tutorial package. +The ``view_page``, ``add_page`` and ``edit_page`` views that we've added +reference a :term:`template`. Each template is a :term:`Chameleon` :term:`ZPT` +template. These templates will live in the ``templates`` directory of our +tutorial package. Chameleon templates must have a ``.pt`` extension to be +recognized as such. The ``view.pt`` Template ------------------------ -The ``view.pt`` template is used for viewing a single wiki page. It -is used by the ``view_page`` view function. It should have a ``div`` -that is "structure replaced" with the ``content`` value provided by -the view. It should also have a link on the rendered page that points -at the "edit" URL (the URL which invokes the ``edit_page`` view for -the page being viewed). - -Once we're done with the ``view.pt`` template, it will look a lot like the -below: +Create ``tutorial/tutorial/templates/view.pt`` and add the following +content: .. literalinclude:: src/views/tutorial/templates/view.pt + :linenos: :language: xml -.. note:: The names available for our use in a template are always - those that are present in the dictionary returned by the view - callable. But our templates make use of a ``request`` object that - none of our tutorial views return in their dictionary. This value - appears as if "by magic". However, ``request`` is one of several - names that are available "by default" in a template when a template - renderer is used. See :ref:`chameleon_template_renderers` for more - information about other names that are available by default in a - template when a Chameleon template is used as a renderer. +This template is used by ``view_page()`` for displaying a single +wiki page. It includes: + +- A ``div`` element that is replaced with the ``content`` + value provided by the view (rows 45-47). ``content`` + contains HTML, so the ``structure`` keyword is used + to prevent escaping it (i.e. changing ">" to >, etc.) +- A link that points + at the "edit" URL which invokes the ``edit_page`` view for + the page being viewed (rows 49-51). The ``edit.pt`` Template ------------------------ -The ``edit.pt`` template is used for adding and editing a wiki page. It is -used by the ``add_page`` and ``edit_page`` view functions. It should display -a page containing a form that POSTs back to the "save_url" argument supplied -by the view. The form should have a "body" textarea field (the page data), -and a submit button that has the name "form.submitted". The textarea in the -form should be filled with any existing page data when it is rendered. - -Once we're done with the ``edit.pt`` template, it will look a lot like -the following: +Create ``tutorial/tutorial/templates/edit.pt`` and add the following +content: .. literalinclude:: src/views/tutorial/templates/edit.pt + :linenos: :language: xml +This template is used by ``add_page()`` and ``edit_page()`` for adding +and editing a wiki page. It displays +a page containing a form that includes: + +- A 10 row by 60 column ``textarea`` field named ``body`` that is filled + with any existing page data when it is rendered (rows 46-47). +- A submit button that has the name ``form.submitted`` (row 48). + +The form POSTs back to the "save_url" argument supplied +by the view (row 45). The view will use the ``body`` and +``form.submitted`` values. + +.. note:: Our templates use a ``request`` object that + none of our tutorial views return in their dictionary. + ``request`` is one of several + names that are available "by default" in a template when a template + renderer is used. See :ref:`chameleon_template_renderers` for + information about other names that are available by default + when a Chameleon template is used as a renderer. + Static Assets ------------- @@ -339,25 +350,24 @@ Viewing the Application in a Browser ==================================== We can finally examine our application in a browser (See -:ref:`wiki2-start-the-application`). The views we'll try are -as follows: +:ref:`wiki2-start-the-application`). Launch a browser and visit +each of the following URLs, check that the result is as expected: -- Visiting ``http://localhost:6543`` in a browser invokes the +- ``http://localhost:6543`` in a browser invokes the ``view_wiki`` view. This always redirects to the ``view_page`` view of the FrontPage page object. -- Visiting ``http://localhost:6543/FrontPage`` in a browser invokes +- ``http://localhost:6543/FrontPage`` in a browser invokes the ``view_page`` view of the front page page object. -- Visiting ``http://localhost:6543/FrontPage/edit_page`` in a browser +- ``http://localhost:6543/FrontPage/edit_page`` in a browser invokes the edit view for the front page object. -- Visiting ``http://localhost:6543/add_page/SomePageName`` in a +- ``http://localhost:6543/add_page/SomePageName`` in a browser invokes the add view for a page. -Try generating an error within the body of a view by adding code to -the top of it that generates an exception (e.g. ``raise -Exception('Forced Exception')``). Then visit the error-raising view -in a browser. You should see an interactive exception handler in the -browser which allows you to examine values in a post-mortem mode. +- To generate an error, visit ``http://localhost:6543/add_page`` which + will generate a ``NoResultFound: No row was found for one()`` error. + You'll see an interactive traceback facility provided + by :term:`pyramid_debugtoolbar`. diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst index 1ff000549..2e6fc0e77 100644 --- a/docs/tutorials/wiki2/design.rst +++ b/docs/tutorials/wiki2/design.rst @@ -36,9 +36,16 @@ page. Views ----- -There will be four views to handle the normal operations of adding and -editing wiki pages, and viewing pages and the wiki front page. Two -additional views will handle the login and logout tasks related to security. +There will be three views to handle the normal operations of adding, +editing and viewing wiki pages, plus one view for the wiki front page. +Two templates will be used, one for viewing, and one for both for adding +and editing wiki pages. + +The default templating systems in :app:`Pyramid` are +:term:`Chameleon` and :term:`Mako`. Chameleon is a variant of +:term:`ZPT`, which is an XML-based templating language. Mako is a +non-XML-based templating language. Because we had to pick one, +we chose Chameleon for this tutorial. Security -------- @@ -67,6 +74,8 @@ use to do this are below. - Permission declarations are added to the views to assert the security policies as each request is handled. +Two additional views and one template will handle the login and +logout tasks. Summary ------- @@ -87,7 +96,7 @@ listed in the following table: | | | | | | | | | | | | +----------------------+-----------------------+-------------+------------+------------+ -| /edit_page/PageName | Display edit form | edit_page | edit.pt | edit | +| /PageName/edit_page | Display edit form | edit_page | edit.pt | edit | | | with existing | | | | | | content. | | | | | | | | | | diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index 1453cd2e6..c7670b049 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -27,12 +27,14 @@ from .security import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(route_name='view_wiki') +@view_config(route_name='view_wiki', + permission='view') def view_wiki(request): return HTTPFound(location = request.route_url('view_page', pagename='FrontPage')) -@view_config(route_name='view_page', renderer='templates/view.pt') +@view_config(route_name='view_page', renderer='templates/view.pt', + permission='view') def view_page(request): pagename = request.matchdict['pagename'] page = DBSession.query(Page).filter_by(name=pagename).first() diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index 465d98ae1..f2a33af1e 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -27,12 +27,14 @@ from .security import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") -@view_config(route_name='view_wiki') +@view_config(route_name='view_wiki', + permission='view') def view_wiki(request): return HTTPFound(location = request.route_url('view_page', pagename='FrontPage')) -@view_config(route_name='view_page', renderer='templates/view.pt') +@view_config(route_name='view_page', renderer='templates/view.pt', + permission='view') def view_page(request): pagename = request.matchdict['pagename'] session = DBSession() diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index b4138be13..d90897d16 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -21,19 +21,20 @@ Pyramid continues to run on Python 2, but Pyramid is now also Python 3 compatible. To use Pyramid under Python 3, Python 3.2 or better is required. Many Pyramid add-ons are already Python 3 compatible. For example, -``pyramid_debugtoolbar``, ``pyramid_jinja2``, ``pyramid_exclog``, and -``pyramid_tm`` are all Python 3-ready. But some are still known to work only -under Python 2. Also, some scaffolding dependencies (particularly ZODB) do -not yet work under Python 3. - -Python 3 compatibility also required dropping some package dependencies and -support for older Python versions and platforms. See the "Backwards -Incompatibilities" section below for more information. +``pyramid_debugtoolbar``, ``pyramid_jinja2``, ``pyramid_exclog``, +``pyramid_tm``, ``pyramid_mailer``, and ``pyramid_handlers`` are all Python +3-ready. But other add-ons are known to work only under Python 2. Also, +some scaffolding dependencies (particularly ZODB) do not yet work under +Python 3. Please be patient as we gain full ecosystem support for Python 3. You can see more details about ongoing porting efforts at https://github.com/Pylons/pyramid/wiki/Python-3-Porting . +Python 3 compatibility required dropping some package dependencies and +support for older Python versions and platforms. See the "Backwards +Incompatibilities" section below for more information. + The ``paster`` Command Has Been Replaced ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index eb4442e98..ccbf3bbe9 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -10,6 +10,7 @@ from pyramid.interfaces import ( ) from pyramid.traversal import DefaultRootFactory +from pyramid.util import InstancePropertyMixin class FactoriesConfiguratorMixin(object): @action_method @@ -118,8 +119,8 @@ class FactoriesConfiguratorMixin(object): """ callable = self.maybe_dotted(callable) - if name is None: - name = callable.__name__ + name, callable = InstancePropertyMixin._make_property( + callable, name=name, reify=reify) def register(): plist = self.registry.queryUtility(IRequestProperties) @@ -130,7 +131,7 @@ class FactoriesConfiguratorMixin(object): self.registry.registerHandler(_set_request_properties, (INewRequest,)) - plist.append((name, callable, reify)) + plist.append((name, callable)) intr = self.introspectable('request properties', name, self.object_description(callable), @@ -143,6 +144,4 @@ class FactoriesConfiguratorMixin(object): def _set_request_properties(event): request = event.request plist = request.registry.queryUtility(IRequestProperties) - for prop in plist: - name, callable, reify = prop - request.set_property(callable, name=name, reify=reify) + request._set_properties(plist) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index a79d20d7d..ad4df28d8 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -865,7 +865,8 @@ class ViewsConfiguratorMixin(object): declaration with this argument ensures that the view will only be called when the request's ``method`` attribute (aka the ``REQUEST_METHOD`` of the WSGI environment) string matches a - supplied value. + supplied value. Note that use of ``GET`` also implies that the + view will respond to ``HEAD`` as of Pyramid 1.4. .. note:: The ability to pass a tuple of items as ``request_method`` is new as of Pyramid 1.2. Previous @@ -996,6 +997,9 @@ class ViewsConfiguratorMixin(object): if request_method is not None: request_method = as_sorted_tuple(request_method) + if 'GET' in request_method and 'HEAD' not in request_method: + # GET implies HEAD too + request_method = as_sorted_tuple(request_method + ('HEAD',)) order, predicates, phash = make_predicates(xhr=xhr, request_method=request_method, path_info=path_info, diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index b2db28ba7..208e54bf5 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -33,7 +33,8 @@ class PkgResourceTemplateLookup(TemplateLookup): """Called from within a Mako template, avoids adjusting the uri if it looks like an asset specification""" # Don't adjust asset spec names - if ':' in uri: + isabs = os.path.isabs(uri) + if (not isabs) and (':' in uri): return uri return TemplateLookup.adjust_uri(self, uri, relativeto) @@ -48,16 +49,21 @@ class PkgResourceTemplateLookup(TemplateLookup): """ isabs = os.path.isabs(uri) if (not isabs) and (':' in uri): + # Windows can't cope with colons in filenames, so we replace the + # colon with a dollar sign in the filename mako uses to actually + # store the generated python code in the mako module_directory or + # in the temporary location of mako's modules + adjusted = uri.replace(':', '$') try: if self.filesystem_checks: - return self._check(uri, self._collection[uri]) + return self._check(adjusted, self._collection[adjusted]) else: - return self._collection[uri] + return self._collection[adjusted] except KeyError: pname, path = resolve_asset_spec(uri) srcfile = abspath_from_asset_spec(path, pname) if os.path.isfile(srcfile): - return self._load(srcfile, uri) + return self._load(srcfile, adjusted) raise exceptions.TopLevelLookupException( "Can not locate template for uri %r" % uri) return TemplateLookup.get_template(self, uri) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 14941c61a..b393a40a6 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -144,17 +144,6 @@ def get_renderer(renderer_name, package=None): # concrete renderer factory implementations (also API) -def json_renderer_factory(info): - def _render(value, system): - request = system.get('request') - if request is not None: - response = request.response - ct = response.content_type - if ct == response.default_content_type: - response.content_type = 'application/json' - return json.dumps(value) - return _render - def string_renderer_factory(info): def _render(value, system): if not isinstance(value, string_types): @@ -168,10 +157,88 @@ def string_renderer_factory(info): return value return _render -class JSONP(object): +class JSON(object): + """ Renderer that returns a JSON-encoded string. + + Configure a custom JSON renderer using the + :meth:`~pyramid.config.Configurator.add_renderer` API at application + startup time: + + .. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.add_renderer('myjson', JSON(indent=4)) + + Once this renderer is registered as above, you can use + ``myjson`` as the ``renderer=`` parameter to ``@view_config`` or + :meth:`~pyramid.config.Configurator.add_view``: + + .. code-block:: python + + from pyramid.view import view_config + + @view_config(renderer='myjson') + def myview(request): + return {'greeting':'Hello world'} + + .. note:: + + This feature is new in Pyramid 1.4. Prior to 1.4 there was + no public API for supplying options to the underlying + :func:`json.dumps` without defining a custom renderer. + + You can pass a ``default`` argument to this class' constructor (which + should be a function) to customize what happens when it attempts to + serialize types unrecognized by the base ``json`` module. See + :ref:`json_serializing_custom_objects` for more information. + """ + + def __init__(self, **kw): + """ Any keyword arguments will be passed to the ``json.dumps`` + function. A notable exception is the keyword argument ``default``, + which is wrapped in a function that sniffs for ``__json__`` + attributes before it is passed along to ``json.dumps``""" + # we wrap the default callback with our own to get __json__ attr + # sniffing + self._default = kw.pop('default', None) + self.kw = kw + + def __call__(self, info): + """ Returns a plain JSON-encoded string with content-type + ``application/json``. The content-type may be overridden by + setting ``request.response.content_type``.""" + def _render(value, system): + request = system.get('request') + if request is not None: + response = request.response + ct = response.content_type + if ct == response.default_content_type: + response.content_type = 'application/json' + return self._dumps(value) + return _render + + def _default_encode(self, obj): + if hasattr(obj, '__json__'): + return obj.__json__() + + if self._default is not None: + return self._default(obj) + raise TypeError('%r is not JSON serializable' % (obj,)) + + def _dumps(self, obj): + """ Encode a Python object to a JSON string. + + By default, this uses the :func:`json.dumps` from the stdlib.""" + return json.dumps(obj, default=self._default_encode, **self.kw) + +json_renderer_factory = JSON() # bw compat + +class JSONP(JSON): """ `JSONP <http://en.wikipedia.org/wiki/JSONP>`_ renderer factory helper which implements a hybrid json/jsonp renderer. JSONP is useful for - making cross-domain AJAX requests. + making cross-domain AJAX requests. Configure a JSONP renderer using the :meth:`pyramid.config.Configurator.add_renderer` API at application @@ -184,6 +251,24 @@ class JSONP(object): config = Configurator() config.add_renderer('jsonp', JSONP(param_name='callback')) + The class' constructor also accepts arbitrary keyword arguments. All + keyword arguments except ``param_name`` are passed to the ``json.dumps`` + function as its keyword arguments. + + .. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.add_renderer('jsonp', JSONP(param_name='callback', indent=4)) + + .. note:: The ability of this class to accept a ``**kw`` in its + constructor is new as of Pyramid 1.4. + + The arguments passed to this class' constructor mean the same thing as + the arguments passed to :class:`pyramid.renderers.JSON` (including + ``default``). + Once this renderer is registered via :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use ``jsonp`` as the ``renderer=`` parameter to ``@view_config`` or @@ -210,9 +295,10 @@ class JSONP(object): See also: :ref:`jsonp_renderer`. """ - - def __init__(self, param_name='callback'): + + def __init__(self, param_name='callback', **kw): self.param_name = param_name + JSON.__init__(self, **kw) def __call__(self, info): """ Returns JSONP-encoded string with content-type @@ -221,7 +307,7 @@ class JSONP(object): plain-JSON encoded string with content-type ``application/json``""" def _render(value, system): request = system['request'] - val = json.dumps(value) + val = self._dumps(value) callback = request.GET.get(self.param_name) if callback is None: ct = 'application/json' diff --git a/pyramid/scaffolds/alchemy/README.txt_tmpl b/pyramid/scaffolds/alchemy/README.txt_tmpl index efea71c5c..9e4aa1125 100644 --- a/pyramid/scaffolds/alchemy/README.txt_tmpl +++ b/pyramid/scaffolds/alchemy/README.txt_tmpl @@ -8,7 +8,7 @@ Getting Started - $venv/bin/python setup.py develop -- $venv/bin/populate_{{project}} development.ini +- $venv/bin/initialize_{{project}}_db development.ini - $venv/bin/pserve development.ini diff --git a/pyramid/scaffolds/alchemy/setup.py_tmpl b/pyramid/scaffolds/alchemy/setup.py_tmpl index b80fc52a8..2d8ed028f 100644 --- a/pyramid/scaffolds/alchemy/setup.py_tmpl +++ b/pyramid/scaffolds/alchemy/setup.py_tmpl @@ -22,7 +22,7 @@ setup(name='{{project}}', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], diff --git a/pyramid/scaffolds/starter/setup.py_tmpl b/pyramid/scaffolds/starter/setup.py_tmpl index 39ac6de9d..58c0a79fc 100644 --- a/pyramid/scaffolds/starter/setup.py_tmpl +++ b/pyramid/scaffolds/starter/setup.py_tmpl @@ -18,7 +18,7 @@ setup(name='{{project}}', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], diff --git a/pyramid/scaffolds/zodb/setup.py_tmpl b/pyramid/scaffolds/zodb/setup.py_tmpl index 965c0178f..acdf095d5 100644 --- a/pyramid/scaffolds/zodb/setup.py_tmpl +++ b/pyramid/scaffolds/zodb/setup.py_tmpl @@ -21,7 +21,7 @@ setup(name='{{project}}', long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py index 5423a6316..13406372a 100644 --- a/pyramid/scripts/prequest.py +++ b/pyramid/scripts/prequest.py @@ -112,6 +112,7 @@ class PRequestCommand(object): 'SERVER_PORT': '80', # always mandatory 'SERVER_PROTOCOL': 'HTTP/1.0', 'CONTENT_TYPE': 'text/plain', + 'REMOTE_ADDR':'127.0.0.1', 'wsgi.run_once': True, 'wsgi.multithread': False, 'wsgi.multiprocess': False, diff --git a/pyramid/security.py b/pyramid/security.py index f29edd678..4b929241e 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -100,7 +100,7 @@ def effective_principals(request): policy = reg.queryUtility(IAuthenticationPolicy) if policy is None: - return [] + return [Everyone] return policy.effective_principals(request) def principals_allowed_by_permission(context, permission): diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 0930f9603..1dfeda34c 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -73,7 +73,7 @@ class TestFactoriesMixin(unittest.TestCase): callable = lambda x: None config.set_request_property(callable, name='foo') plist = config.registry.getUtility(IRequestProperties) - self.assertEqual(plist, [('foo', callable, False)]) + self.assertEqual(set(p[0] for p in plist), set(['foo'])) def test_set_request_property_with_unnamed_callable(self): from pyramid.interfaces import IRequestProperties @@ -81,7 +81,7 @@ class TestFactoriesMixin(unittest.TestCase): def foo(self): pass config.set_request_property(foo, reify=True) plist = config.registry.getUtility(IRequestProperties) - self.assertEqual(plist, [('foo', foo, True)]) + self.assertEqual(set(p[0] for p in plist), set(['foo'])) def test_set_request_property_with_property(self): from pyramid.interfaces import IRequestProperties @@ -89,7 +89,7 @@ class TestFactoriesMixin(unittest.TestCase): callable = property(lambda x: None) config.set_request_property(callable, name='foo') plist = config.registry.getUtility(IRequestProperties) - self.assertEqual(plist, [('foo', callable, False)]) + self.assertEqual(set(p[0] for p in plist), set(['foo'])) def test_set_multiple_request_properties(self): from pyramid.interfaces import IRequestProperties @@ -100,8 +100,7 @@ class TestFactoriesMixin(unittest.TestCase): config.set_request_property(bar, name='bar') config.commit() plist = config.registry.getUtility(IRequestProperties) - self.assertEqual(plist, [('foo', foo, True), - ('bar', bar, False)]) + self.assertEqual(set(p[0] for p in plist), set(['foo', 'bar'])) def test_set_multiple_request_properties_conflict(self): from pyramid.exceptions import ConfigurationConflictError @@ -125,20 +124,19 @@ class TestFactoriesMixin(unittest.TestCase): request = DummyRequest(config.registry) event = Event() config.registry.notify(event) - callables = event.request.callables - self.assertEqual(callables, [('foo', foo, False), - ('bar', foo, True)]) + plist = event.request.plist + self.assertEqual(set(p[0] for p in plist), set(['foo', 'bar'])) class DummyRequest(object): - callables = None + plist = None def __init__(self, registry): self.registry = registry - def set_property(self, callable, name, reify): - if self.callables is None: - self.callables = [] - self.callables.append((name, callable, reify)) + def _set_properties(self, properties): + if self.plist is None: + self.plist = [] + self.plist.extend(properties) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index d44bec691..9b46f83c9 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1146,6 +1146,16 @@ class TestViewsConfigurationMixin(unittest.TestCase): request.method = 'GET' self._assertNotFound(wrapper, None, request) + def test_add_view_with_request_method_get_implies_head(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_method='GET', renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.method = 'HEAD' + self.assertEqual(wrapper(None, request), 'OK') + def test_add_view_with_request_param_noval_true(self): from pyramid.renderers import null_renderer view = lambda *arg: 'OK' @@ -2342,7 +2352,7 @@ class TestViewDeriver(unittest.TestCase): msg = e.args[0] self.assertTrue(msg.startswith( 'Could not convert return value of the view callable object ' - '<pyramid.tests.test_config.test_views.AView object at')) + '<pyramid.tests.test_config.test_views.')) self.assertTrue(msg.endswith( '> into a response object. The value returned was None. You ' 'may have forgotten to return a value from the view callable.')) diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index 550c95312..fbb04273b 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -1,7 +1,11 @@ ## come on python gimme some of that sweet, sweet -*- coding: utf-8 -*- +import shutil +import tempfile import unittest + from pyramid import testing + from pyramid.compat import ( text_, text_type, @@ -466,6 +470,15 @@ class TestPkgResourceTemplateLookup(unittest.TestCase): result = inst.get_template('pyramid.tests:fixtures/helloworld.mak') self.assertFalse(result is None) + def test_get_template_asset_spec_with_module_dir(self): + tmpdir = tempfile.mkdtemp() + try: + inst = self._makeOne(module_directory=tmpdir) + result = inst.get_template('pyramid.tests:fixtures/helloworld.mak') + self.assertFalse(result is None) + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + def test_get_template_asset_spec_missing(self): from mako.exceptions import TopLevelLookupException fixturedir = self.get_fixturedir() diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index b32e68e25..55ed3f7fd 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -340,35 +340,64 @@ class TestChameleonRendererLookup(unittest.TestCase): self.assertNotEqual(reg.queryUtility(ITemplateRenderer, name=spec), None) -class Test_json_renderer_factory(unittest.TestCase): +class TestJSON(unittest.TestCase): def setUp(self): self.config = testing.setUp() def tearDown(self): testing.tearDown() - - def _callFUT(self, name): - from pyramid.renderers import json_renderer_factory - return json_renderer_factory(name) + + def _makeOne(self, **kw): + from pyramid.renderers import JSON + return JSON(**kw) def test_it(self): - renderer = self._callFUT(None) + renderer = self._makeOne()(None) result = renderer({'a':1}, {}) self.assertEqual(result, '{"a": 1}') def test_with_request_content_type_notset(self): request = testing.DummyRequest() - renderer = self._callFUT(None) + renderer = self._makeOne()(None) renderer({'a':1}, {'request':request}) self.assertEqual(request.response.content_type, 'application/json') def test_with_request_content_type_set(self): request = testing.DummyRequest() request.response.content_type = 'text/mishmash' - renderer = self._callFUT(None) + renderer = self._makeOne()(None) renderer({'a':1}, {'request':request}) self.assertEqual(request.response.content_type, 'text/mishmash') + def test_with_custom_encoder(self): + from datetime import datetime + def default(obj): + return obj.isoformat() + now = datetime.utcnow() + renderer = self._makeOne(default=default)(None) + result = renderer({'a':now}, {}) + self.assertEqual(result, '{"a": "%s"}' % now.isoformat()) + + def test_with_object_encoder(self): + class MyObject(object): + def __init__(self, x): + self.x = x + def __json__(self): + return {'x': self.x} + + objects = [MyObject(1), MyObject(2)] + renderer = self._makeOne()(None) + result = renderer(objects, {}) + self.assertEqual(result, '[{"x": 1}, {"x": 2}]') + + def test_with_object_encoder_no___json__(self): + class MyObject(object): + def __init__(self, x): + self.x = x + objects = [MyObject(1), MyObject(2)] + renderer = self._makeOne()(None) + self.assertRaises(TypeError, renderer, objects, {}) + class Test_string_renderer_factory(unittest.TestCase): def _callFUT(self, name): from pyramid.renderers import string_renderer_factory diff --git a/pyramid/tests/test_scaffolds/test_copydir.py b/pyramid/tests/test_scaffolds/test_copydir.py index 42edd9d23..68cefbe6e 100644 --- a/pyramid/tests/test_scaffolds/test_copydir.py +++ b/pyramid/tests/test_scaffolds/test_copydir.py @@ -170,9 +170,11 @@ class Test_makedirs(unittest.TestCase): def test_makedirs_parent_dir(self): import shutil - target = "/tmp/nonexistent_dir/nonexistent_subdir" + import tempfile + tmpdir = tempfile.mkdtemp() + target = os.path.join(tmpdir, 'nonexistent_subdir') self._callFUT(target, 2, None) - shutil.rmtree("/tmp/nonexistent_dir") + shutil.rmtree(tmpdir) class Test_support_functions(unittest.TestCase): diff --git a/pyramid/tests/test_scripts/test_proutes.py b/pyramid/tests/test_scripts/test_proutes.py index 8c2660465..fad8c1895 100644 --- a/pyramid/tests/test_scripts/test_proutes.py +++ b/pyramid/tests/test_scripts/test_proutes.py @@ -100,7 +100,8 @@ class TestPRoutesCommand(unittest.TestCase): result = command.run() self.assertEqual(result, 0) self.assertEqual(len(L), 3) - self.assertEqual(L[-1].split()[:4], ['a', '/a', '<function', 'view']) + compare_to = L[-1].split()[:3] + self.assertEqual(compare_to, ['a', '/a', '<function']) def test_single_route_one_view_registered_with_factory(self): from zope.interface import Interface diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index 86149d554..ba9538b01 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -266,9 +266,10 @@ class TestEffectivePrincipals(unittest.TestCase): return effective_principals(request) def test_no_authentication_policy(self): + from pyramid.security import Everyone request = _makeRequest() result = self._callFUT(request) - self.assertEqual(result, []) + self.assertEqual(result, [Everyone]) def test_with_authentication_policy(self): request = _makeRequest() diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 824ee329f..e83ad5922 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -3,12 +3,12 @@ from pyramid.compat import PY3 class Test_InstancePropertyMixin(unittest.TestCase): def _makeOne(self): - cls = self._targetClass() + cls = self._getTargetClass() class Foo(cls): pass return Foo() - def _targetClass(self): + def _getTargetClass(self): from pyramid.util import InstancePropertyMixin return InstancePropertyMixin @@ -109,6 +109,29 @@ class Test_InstancePropertyMixin(unittest.TestCase): foo.set_property(lambda _: 2, name='x', reify=True) self.assertEqual(1, foo.x) + def test__make_property(self): + from pyramid.decorator import reify + cls = self._getTargetClass() + name, fn = cls._make_property(lambda x: 1, name='x', reify=True) + self.assertEqual(name, 'x') + self.assertTrue(isinstance(fn, reify)) + + def test__set_properties_with_iterable(self): + foo = self._makeOne() + x = foo._make_property(lambda _: 1, name='x', reify=True) + y = foo._make_property(lambda _: 2, name='y') + foo._set_properties([x, y]) + self.assertEqual(1, foo.x) + self.assertEqual(2, foo.y) + + def test__set_properties_with_dict(self): + foo = self._makeOne() + x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True) + y_name, y_fn = foo._make_property(lambda _: 2, name='y') + foo._set_properties({x_name: x_fn, y_name: y_fn}) + self.assertEqual(1, foo.x) + self.assertEqual(2, foo.y) + class Test_WeakOrderedSet(unittest.TestCase): def _makeOne(self): from pyramid.config import WeakOrderedSet diff --git a/pyramid/util.py b/pyramid/util.py index cca1872b7..7d5c97814 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -20,6 +20,60 @@ class InstancePropertyMixin(object): on the class itself. """ + @classmethod + def _make_property(cls, callable, name=None, reify=False): + """ Convert a callable into one suitable for adding to the + instance. This will return a 2-tuple containing the computed + (name, property) pair. + """ + + is_property = isinstance(callable, property) + if is_property: + fn = callable + if name is None: + raise ValueError('must specify "name" for a property') + if reify: + raise ValueError('cannot reify a property') + elif name is not None: + fn = lambda this: callable(this) + fn.__name__ = name + fn.__doc__ = callable.__doc__ + else: + name = callable.__name__ + fn = callable + if reify: + import pyramid.decorator # avoid circular import + fn = pyramid.decorator.reify(fn) + elif not is_property: + fn = property(fn) + + return name, fn + + def _set_properties(self, properties): + """ Create several properties on the instance at once. + + This is a more efficient version of + :meth:`pyramid.util.InstancePropertyMixin.set_property` which + can accept multiple ``(name, property)`` pairs generated via + :meth:`pyramid.util.InstancePropertyMixin._make_property`. + + ``attrs`` is a sequence of 2-tuples *or* a data structure with + an ``.items()`` method which returns a sequence of 2-tuples + (presumably a dictionary). It will be used used to add several + properties to the instance in a manner that is more efficient + than simply calling ``set_property`` repeatedly. + """ + + if hasattr(properties, 'items'): + attrs = properties.items() + else: + attrs = properties + attrs = dict(properties) + + parent = self.__class__ + cls = type(parent.__name__, (parent, object), attrs) + self.__class__ = cls + def set_property(self, callable, name=None, reify=False): """ Add a callable or a property descriptor to the instance. @@ -31,12 +85,11 @@ class InstancePropertyMixin(object): A property may also be reified via the :class:`pyramid.decorator.reify` decorator by setting ``reify=True``, allowing the result of the evaluation to be - cached. Thus the value of the property is only computed once for - the lifetime of the object. + cached. Using this method, the value of the property is only + computed once for the lifetime of the object. ``callable`` can either be a callable that accepts the instance - as - its single positional parameter, or it can be a property + as its single positional parameter, or it can be a property descriptor. If the ``callable`` is a property descriptor, the ``name`` @@ -73,30 +126,8 @@ class InstancePropertyMixin(object): >>> foo.y # notice y keeps the original value 1 """ - - is_property = isinstance(callable, property) - if is_property: - fn = callable - if name is None: - raise ValueError('must specify "name" for a property') - if reify: - raise ValueError('cannot reify a property') - elif name is not None: - fn = lambda this: callable(this) - fn.__name__ = name - fn.__doc__ = callable.__doc__ - else: - name = callable.__name__ - fn = callable - if reify: - import pyramid.decorator - fn = pyramid.decorator.reify(fn) - elif not is_property: - fn = property(fn) - attrs = { name: fn } - parent = self.__class__ - cls = type(parent.__name__, (parent, object), attrs) - self.__class__ = cls + prop = self._make_property(callable, name=name, reify=reify) + self._set_properties([prop]) class WeakOrderedSet(object): """ Maintain a set of items. diff --git a/pyramid/view.py b/pyramid/view.py index d722c0cbb..bb531c326 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -335,7 +335,7 @@ class notfound_view_config(object): from pyramid.view import notfound_view_config from pyramid.response import Response - notfound_view_config() + @notfound_view_config() def notfound(request): return Response('Not found, dude!', status='404 Not Found') @@ -409,7 +409,7 @@ class forbidden_view_config(object): from pyramid.view import forbidden_view_config from pyramid.response import Response - forbidden_view_config() + @forbidden_view_config() def notfound(request): return Response('You are not allowed', status='401 Unauthorized') @@ -10,4 +10,4 @@ cover-erase=1 [aliases] dev = develop easy_install pyramid[testing] - +docs = develop easy_install pyramid[docs] @@ -50,21 +50,25 @@ install_requires=[ tests_require = [ 'WebTest >= 1.3.1', # py3 compat - 'virtualenv', ] if not PY3: - tests_require.extend([ - 'Sphinx', - 'docutils', - 'repoze.sphinx.autointerface', - 'zope.component>=3.11.0', - ]) + tests_require.append('zope.component>=3.11.0') -testing_extras = tests_require + ['nose', 'coverage'] +docs_extras = [ + 'Sphinx', + 'docutils', + 'repoze.sphinx.autointerface', + ] + +testing_extras = tests_require + [ + 'nose', + 'coverage', + 'virtualenv', # for scaffolding tests + ] setup(name='pyramid', - version='1.3b3', + version='1.4dev', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, @@ -77,7 +81,7 @@ setup(name='pyramid', "Programming Language :: Python :: 3.2", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", - "Framework :: Pylons", + "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI", "License :: Repoze Public License", @@ -93,6 +97,7 @@ setup(name='pyramid', install_requires = install_requires, extras_require = { 'testing':testing_extras, + 'docs':docs_extras, }, tests_require = tests_require, test_suite="pyramid.tests", |
