Chris McDonough
2013-08-27 8ac9ae83d7deb7909733e6490cad52beb88190ad
Merge branch 'master' of github.com:Pylons/pyramid
7 files modified
206 ■■■■■ changed files
CHANGES.txt 4 ●●●● patch | view | raw | blame | history
CONTRIBUTORS.txt 2 ●●●●● patch | view | raw | blame | history
docs/narr/i18n.rst 58 ●●●● patch | view | raw | blame | history
docs/narr/urldispatch.rst 35 ●●●●● patch | view | raw | blame | history
pyramid/config/routes.py 35 ●●●●● patch | view | raw | blame | history
pyramid/httpexceptions.py 1 ●●●● patch | view | raw | blame | history
pyramid/tests/test_url.py 71 ●●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -142,6 +142,10 @@
- The ``alchemy`` scaffold tests now provide better coverage.  See
  https://github.com/Pylons/pyramid/pull/1029
- The ``pyramid.config.Configurator.add_route`` method now supports being
  called with an external URL as pattern. See
  https://github.com/Pylons/pyramid/issues/611 for more information.
Bug Fixes
---------
CONTRIBUTORS.txt
@@ -216,3 +216,5 @@
- Tom Lazar, 2013/08/15
- Andreas Zeidler, 2013/08/15
- Matthew Wilkes, 2013/08/23
docs/narr/i18n.rst
@@ -579,18 +579,9 @@
   def pluralize(singular, plural, n, domain=None, mapping=None):
       ...
The ``singular`` and ``plural`` arguments should each be a Unicode
value representing a :term:`message identifier`.  ``n`` should be an
integer.  ``domain`` should be a :term:`translation domain`, and
``mapping`` should be a dictionary that is used for *replacement
value* interpolation of the translated string.  If ``n`` is plural
for the current locale, ``pluralize`` will return a Unicode
translation for the message id ``plural``, otherwise it will return a
Unicode translation for the message id ``singular``.
The arguments provided as ``singular`` and/or ``plural`` may also be
:term:`translation string` objects, but the domain and mapping
information attached to those objects is ignored.
The simplest case is the ``singular`` and ``plural`` arguments being passed as
unicode literals. This returns the appropriate literal according to the locale
pluralization rules for the number ``n``, and interpolates ``mapping``.
.. code-block:: python
   :linenos:
@@ -602,6 +593,49 @@
       translated = localizer.pluralize('Item', 'Items', 1, 'mydomain')
       # ... use translated ...
However, for support of other languages, the ``singular`` argument should
be a Unicode value representing a :term:`message identifier`.  In this
case the ``plural`` value is ignored.
``domain`` should be a :term:`translation domain`, and
``mapping`` should be a dictionary that is used for *replacement
value* interpolation of the translated string.
The value of ``n`` will be used to find the appropriate plural form for the
current language and ``pluralize`` will return a Unicode translation for the
message id ``singular``. The message file must have defined ``singular`` as a
translation with plural forms.
The argument provided as ``singular`` may be a :term:`translation string`
object, but the domain and mapping information attached is ignored.
.. code-block:: python
   :linenos:
   from pyramid.i18n import get_localizer
   def aview(request):
       localizer = get_localizer(request)
       num = 1
       translated = localizer.pluralize(_('item_plural', default="${number} items"),
           None, num, 'mydomain', mapping={'number':num})
The corresponding message catalog must have language plural definitions and
plural alternatives set.
.. code-block:: text
    :linenos:
    "Plural-Forms: nplurals=3; plural=n==0 ? 0 : n==1 ? 1 : 2;"
    msgid "item_plural"
    msgid_plural ""
    msgstr[0] "No items"
    msgstr[1] "${number} item"
    msgstr[2] "${number} items"
More information on complex plurals can be found in the `gettext documentation
<https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/Plural-forms.html>`_.
.. index::
   single: locale name
   single: get_locale_name
docs/narr/urldispatch.rst
@@ -105,6 +105,7 @@
.. _route_pattern_syntax:
Route Pattern Syntax
~~~~~~~~~~~~~~~~~~~~
@@ -126,6 +127,10 @@
.. code-block:: text
   /{foo}/bar/baz
If a pattern is a valid URL it won't be ever matched against an incoming
request. Instead it can be useful for generating external URLs. See
:ref:`External routes <external_route_narr>` for details.
A pattern segment (an individual item between ``/`` characters in the
pattern) may either be a literal string (e.g. ``foo``) *or* it may be a
@@ -754,9 +759,39 @@
exception to this rule is use of the ``pregenerator`` argument, which is not
ignored when ``static`` is ``True``.
:ref:`External routes <external_route_narr>` are implicitly static.
.. versionadded:: 1.1
   the ``static`` argument to :meth:`~pyramid.config.Configurator.add_route`
.. _external_route_narr:
External Routes
---------------
.. versionadded:: 1.5
Route patterns that are valid URLs, are treated as external routes. Like
:ref:`static routes <static_route_narr>` they are useful for URL generation
purposes only and are never considered for matching at request time.
.. code-block:: python
   :linenos:
   >>> config = Configurator()
   >>> config.add_route('youtube', 'https://youtube.com/watch/{video_id}')
   ...
   >>> request.route_url('youtube', video_id='oHg5SJYRHA0')
   >>> "https://youtube.com/watch/oHg5SJYRHA0"
Most pattern replacements and calls to
:meth:`pyramid.request.Request.route_url` will work as expected. However, calls
to :meth:`pyramid.request.Request.route_path` against external patterns will
raise an exception, and passing ``_app_url`` to
:meth:`~pyramid.request.Request.route_url` to generate a URL against a route
that has an external pattern will also raise an exception.
.. index::
   single: redirecting to slash-appended routes
pyramid/config/routes.py
@@ -1,5 +1,6 @@
import warnings
from pyramid.compat import urlparse
from pyramid.interfaces import (
    IRequest,
    IRouteRequest,
@@ -384,7 +385,39 @@
        if pattern is None:
            raise ConfigurationError('"pattern" argument may not be None')
        if self.route_prefix:
        # check for an external route; an external route is one which is
        # is a full url (e.g. 'http://example.com/{id}')
        parsed = urlparse.urlparse(pattern)
        if parsed.hostname:
            pattern = parsed.path
            original_pregenerator = pregenerator
            def external_url_pregenerator(request, elements, kw):
                if '_app_url' in kw:
                    raise ValueError(
                        'You cannot generate a path to an external route '
                        'pattern via request.route_path nor pass an _app_url '
                        'to request.route_url when generating a URL for an '
                        'external route pattern (pattern was "%s") ' %
                        (pattern,)
                        )
                if '_scheme' in kw:
                    scheme = kw['_scheme']
                elif parsed.scheme:
                    scheme = parsed.scheme
                else:
                    scheme = request.scheme
                kw['_app_url'] = '{0}://{1}'.format(scheme, parsed.netloc)
                if original_pregenerator:
                    elements, kw = original_pregenerator(
                        request, elements, kw)
                return elements, kw
            pregenerator = external_url_pregenerator
            static = True
        elif self.route_prefix:
            pattern = self.route_prefix.rstrip('/') + '/' + pattern.lstrip('/')
        mapper = self.get_routes_mapper()
pyramid/httpexceptions.py
@@ -28,7 +28,6 @@
      * 303 - HTTPSeeOther
      * 304 - HTTPNotModified
      * 305 - HTTPUseProxy
      * 306 - Unused (not implemented, obviously)
      * 307 - HTTPTemporaryRedirect
    HTTPError
      HTTPClientError
pyramid/tests/test_url.py
@@ -1027,6 +1027,77 @@
        self.assertEqual(request.elements, ('abc',))
        self.assertEqual(request.kw, {'_anchor':'abc'})
class Test_external_static_url_integration(unittest.TestCase):
    def setUp(self):
        self.config = testing.setUp()
    def tearDown(self):
        testing.tearDown()
    def _makeRequest(self):
        from pyramid.request import Request
        return Request.blank('/')
    def test_generate_external_url(self):
        self.config.add_route('acme', 'https://acme.org/path/{foo}')
        request = self._makeRequest()
        request.registry = self.config.registry
        self.assertEqual(
            request.route_url('acme', foo='bar'),
            'https://acme.org/path/bar')
    def test_generate_external_url_without_scheme(self):
        self.config.add_route('acme', '//acme.org/path/{foo}')
        request = self._makeRequest()
        request.registry = self.config.registry
        self.assertEqual(
            request.route_url('acme', foo='bar'),
            'http://acme.org/path/bar')
    def test_generate_external_url_with_explicit_scheme(self):
        self.config.add_route('acme', '//acme.org/path/{foo}')
        request = self._makeRequest()
        request.registry = self.config.registry
        self.assertEqual(
            request.route_url('acme', foo='bar', _scheme='https'),
            'https://acme.org/path/bar')
    def test_generate_external_url_with_explicit_app_url(self):
        self.config.add_route('acme', 'http://acme.org/path/{foo}')
        request = self._makeRequest()
        request.registry = self.config.registry
        self.assertRaises(ValueError,
            request.route_url, 'acme', foo='bar', _app_url='http://fakeme.com')
    def test_generate_external_url_route_path(self):
        self.config.add_route('acme', 'https://acme.org/path/{foo}')
        request = self._makeRequest()
        request.registry = self.config.registry
        self.assertRaises(ValueError, request.route_path, 'acme', foo='bar')
    def test_generate_external_url_with_pregenerator(self):
        def pregenerator(request, elements, kw):
            kw['_query'] = {'q': 'foo'}
            return elements, kw
        self.config.add_route('acme', 'https://acme.org/path/{foo}',
                              pregenerator=pregenerator)
        request = self._makeRequest()
        request.registry = self.config.registry
        self.assertEqual(
            request.route_url('acme', foo='bar'),
            'https://acme.org/path/bar?q=foo')
    def test_external_url_with_route_prefix(self):
        def includeme(config):
            config.add_route('acme', '//acme.org/{foo}')
        self.config.include(includeme, route_prefix='some_prefix')
        request = self._makeRequest()
        request.registry = self.config.registry
        self.assertEqual(
            request.route_url('acme', foo='bar'),
            'http://acme.org/bar')
class DummyContext(object):
    def __init__(self, next=None):
        self.next = next