summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2013-08-27 11:18:33 -0400
committerChris McDonough <chrism@plope.com>2013-08-27 11:18:33 -0400
commit8ac9ae83d7deb7909733e6490cad52beb88190ad (patch)
tree312a76ee2c11d6d218a11c3acf8bf77d5df0b326
parent4aec433291dc7b0c08d27fe6352ecf7585052e73 (diff)
parent94b817ae54317652b784fbc9e3a90b7215ed00f5 (diff)
downloadpyramid-8ac9ae83d7deb7909733e6490cad52beb88190ad.tar.gz
pyramid-8ac9ae83d7deb7909733e6490cad52beb88190ad.tar.bz2
pyramid-8ac9ae83d7deb7909733e6490cad52beb88190ad.zip
Merge branch 'master' of github.com:Pylons/pyramid
-rw-r--r--CHANGES.txt4
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--docs/narr/i18n.rst58
-rw-r--r--docs/narr/urldispatch.rst35
-rw-r--r--pyramid/config/routes.py35
-rw-r--r--pyramid/httpexceptions.py1
-rw-r--r--pyramid/tests/test_url.py71
7 files changed, 192 insertions, 14 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index b805a12a0..1eeb0ce7b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -142,6 +142,10 @@ Features
- 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
---------
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index ef02bf7f4..591dcd7e4 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -216,3 +216,5 @@ Contributors
- Tom Lazar, 2013/08/15
- Andreas Zeidler, 2013/08/15
+
+- Matthew Wilkes, 2013/08/23
diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst
index 2964686d3..555b06e0f 100644
--- a/docs/narr/i18n.rst
+++ b/docs/narr/i18n.rst
@@ -579,18 +579,9 @@ signature:
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 @@ information attached to those objects is ignored.
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
diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst
index 310c160c0..62eb89348 100644
--- a/docs/narr/urldispatch.rst
+++ b/docs/narr/urldispatch.rst
@@ -105,6 +105,7 @@ to using the previous combination of ``add_route`` and ``add_view``.
.. _route_pattern_syntax:
+
Route Pattern Syntax
~~~~~~~~~~~~~~~~~~~~
@@ -127,6 +128,10 @@ and:
/{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
replacement marker (e.g. ``{foo}``) or a certain combination of both. A
@@ -754,9 +759,39 @@ other non-``name`` and non-``pattern`` arguments to
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
diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py
index c86e4a2dd..0ed370c94 100644
--- a/pyramid/config/routes.py
+++ b/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 @@ class RoutesConfiguratorMixin(object):
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()
diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py
index c21a717f8..d8832570b 100644
--- a/pyramid/httpexceptions.py
+++ b/pyramid/httpexceptions.py
@@ -28,7 +28,6 @@ Exception
* 303 - HTTPSeeOther
* 304 - HTTPNotModified
* 305 - HTTPUseProxy
- * 306 - Unused (not implemented, obviously)
* 307 - HTTPTemporaryRedirect
HTTPError
HTTPClientError
diff --git a/pyramid/tests/test_url.py b/pyramid/tests/test_url.py
index bf1c514c5..6f1ee3bf0 100644
--- a/pyramid/tests/test_url.py
+++ b/pyramid/tests/test_url.py
@@ -1027,6 +1027,77 @@ class Test_current_route_path(unittest.TestCase):
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