Chris McDonough
2013-08-29 330164c3190d92a3e1df89baafba12570d03bd32
make local_name an attribute of Request, move logic from get_localizer into Request.localizer, fix docs; closes #1099
8 files modified
369 ■■■■ changed files
CHANGES.txt 12 ●●●● patch | view | raw | blame | history
docs/api/request.rst 14 ●●●●● patch | view | raw | blame | history
docs/narr/i18n.rst 77 ●●●●● patch | view | raw | blame | history
pyramid/i18n.py 60 ●●●●● patch | view | raw | blame | history
pyramid/request.py 13 ●●●●● patch | view | raw | blame | history
pyramid/testing.py 4 ●●● patch | view | raw | blame | history
pyramid/tests/test_config/test_i18n.py 13 ●●●● patch | view | raw | blame | history
pyramid/tests/test_i18n.py 176 ●●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -9,8 +9,10 @@
  represent a "bare" ``{{a}}``.  See 
  https://github.com/Pylons/pyramid/pull/862
- Add ``localizer`` property (reified) to the request.
  See https://github.com/Pylons/pyramid/issues/508.
- Add ``localizer`` and ``locale_name`` properties (reified) to the request.
  See https://github.com/Pylons/pyramid/issues/508.  Note that the
  ``pyramid.i18n.get_localizer`` and ``pyramid.i18n.get_locale_name`` functions
  now simply look up these properties on the request.
- Add ``pdistreport`` script, which prints the Python version in use, the
  Pyramid version in use, and the version number and location of all Python
@@ -224,6 +226,12 @@
  as WSGI servers always unquote the slash anyway, and Pyramid never sees the 
  quoted value.
- It is no longer possible to set a ``locale_name`` attribute of the request,
  nor is it possible to set a ``localizer`` attribute of the request.  These
  are now "reified" properties that look up a locale name and localizer
  respectively using the machinery described in the "Internationalization"
  chapter of the documentation.
1.4 (2012-12-18)
================
docs/api/request.rst
@@ -311,6 +311,20 @@
       .. versionadded:: 1.3
   .. attribute::  localizer
       A :term:`localizer` which will use the current locale name to
       translate values.
       .. versionadded:: 1.5
   .. attribute::  locale_name
       The locale name of the current request as computed by the
       :term:`locale negotiator`.
       .. versionadded:: 1.5
.. note::
   For information about the API of a :term:`multidict` structure (such as
docs/narr/i18n.rst
@@ -495,7 +495,6 @@
.. index::
   single: localizer
   single: get_localizer
   single: translation
   single: pluralization
@@ -503,19 +502,17 @@
-----------------
A :term:`localizer` is an object that allows you to perform translation or
pluralization "by hand" in an application.  You may use the
:func:`pyramid.i18n.get_localizer` function to obtain a :term:`localizer`.
This function will return either the localizer object implied by the active
:term:`locale negotiator` or a default localizer object if no explicit locale
negotiator is registered.
pluralization "by hand" in an application.  You may use the
:attr:`pyramid.request.Request.localizer` attribute to obtain a
:term:`localizer`.  The localizer object will be configured to produce
translations implied by the active :term:`locale negotiator` or a default
localizer object if no explicit locale negotiator is registered.
.. code-block:: python
   :linenos:
   from pyramid.i18n import get_localizer
   def aview(request):
       locale = get_localizer(request)
       localizer = request.localizer
.. note::
@@ -538,22 +535,20 @@
.. code-block:: python
   :linenos:
   from pyramid.i18n import get_localizer
   from pyramid.i18n import TranslationString
   ts = TranslationString('Add ${number}', mapping={'number':1},
                          domain='pyramid')
   def aview(request):
       localizer = get_localizer(request)
       localizer = request.localizer
       translated = localizer.translate(ts) # translation string
       # ... use translated ...
The :func:`~pyramid.i18n.get_localizer` function will return a
:class:`pyramid.i18n.Localizer` object bound to the locale name
represented by the request.  The translation returned from its
:meth:`pyramid.i18n.Localizer.translate` method will depend on the
``domain`` attribute of the provided translation string as well as the
The ``request.localizer`` attribute will be a :class:`pyramid.i18n.Localizer`
object bound to the locale name represented by the request.  The translation
returned from its :meth:`pyramid.i18n.Localizer.translate` method will depend
on the ``domain`` attribute of the provided translation string as well as the
locale of the localizer.
.. note::
@@ -586,10 +581,8 @@
.. code-block:: python
   :linenos:
   from pyramid.i18n import get_localizer
   def aview(request):
       localizer = get_localizer(request)
       localizer = request.localizer
       translated = localizer.pluralize('Item', 'Items', 1, 'mydomain')
       # ... use translated ...
@@ -611,13 +604,13 @@
.. code-block:: python
   :linenos:
   from pyramid.i18n import get_localizer
   def aview(request):
       localizer = get_localizer(request)
       localizer = request.localizer
       num = 1
       translated = localizer.pluralize(_('item_plural', default="${number} items"),
           None, num, 'mydomain', mapping={'number':num})
       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.
@@ -638,7 +631,6 @@
.. index::
   single: locale name
   single: get_locale_name
   single: negotiate_locale_name
.. _obtaining_the_locale_name:
@@ -647,25 +639,23 @@
---------------------------------------
You can obtain the locale name related to a request by using the
:func:`pyramid.i18n.get_locale_name` function.
:func:`pyramid.request.Request.locale_name` attribute of the request.
.. code-block:: python
   :linenos:
   from pyramid.i18n import get_locale_name
   def aview(request):
       locale_name = get_locale_name(request)
       locale_name = request.locale_name
This returns the locale name negotiated by the currently active
:term:`locale negotiator` or the :term:`default locale name` if the
locale negotiator returns ``None``.  You can change the default locale
name by changing the ``pyramid.default_locale_name`` setting; see
:ref:`default_locale_name_setting`.
The locale name of a request is dynamically computed; it will be the locale
name negotiated by the currently active :term:`locale negotiator` or
the :term:`default locale name` if the locale negotiator returns ``None``.
You can change the default locale name by changing the
``pyramid.default_locale_name`` setting; see :ref:`default_locale_name_setting`.
Once :func:`~pyramid.i18n.get_locale_name` is first run, the locale
Once :func:`~pyramid.request.Request.locale_name` is first run, the locale
name is stored on the request object.  Subsequent calls to
:func:`~pyramid.i18n.get_locale_name` will return the stored locale
:func:`~pyramid.request.Request.locale_name` will return the stored locale
name without invoking the :term:`locale negotiator`.  To avoid this
caching, you can use the :func:`pyramid.i18n.negotiate_locale_name`
function:
@@ -684,15 +674,13 @@
.. code-block:: python
   :linenos:
   from pyramid.i18n import get_localizer
   def aview(request):
       localizer = get_localizer(request)
       localizer = request.localizer
       locale_name = localizer.locale_name
Obtaining the locale name as an attribute of a localizer is equivalent
to obtaining a locale name by calling the
:func:`~pyramid.i18n.get_locale_name` function.
to obtaining a locale name by asking for the
:func:`~pyramid.request.Request.locale_name` attribute.
.. index::
   single: date and currency formatting (i18n)
@@ -720,10 +708,9 @@
   :linenos:
   from babel.core import Locale
   from pyramid.i18n import get_locale_name
   def aview(request):
       locale_name = get_locale_name(request)
       locale_name = request.locale_name
       locale = Locale(locale_name)
.. index::
@@ -1005,8 +992,8 @@
accepts a request and which returns a :term:`locale name`.  It is
consulted when :meth:`pyramid.i18n.Localizer.translate` or
:meth:`pyramid.i18n.Localizer.pluralize` is invoked.  It is also
consulted when :func:`~pyramid.i18n.get_locale_name` or
:func:`~pyramid.i18n.negotiate_locale_name` is invoked.
consulted when :func:`~pyramid.request.Request.locale_name` is accessed or
when :func:`~pyramid.i18n.negotiate_locale_name` is invoked.
.. _default_locale_negotiator:
pyramid/i18n.py
@@ -12,6 +12,7 @@
TranslationStringFactory = TranslationStringFactory # PyFlakes
from pyramid.compat import PY3
from pyramid.decorator import reify
from pyramid.interfaces import (
    ILocalizer,
@@ -127,7 +128,7 @@
def negotiate_locale_name(request):
    """ Negotiate and return the :term:`locale name` associated with
    the current request (never cached)."""
    the current request."""
    try:
        registry = request.registry
    except AttributeError:
@@ -144,12 +145,9 @@
def get_locale_name(request):
    """ Return the :term:`locale name` associated with the current
    request (possibly cached)."""
    locale_name = getattr(request, 'locale_name', None)
    if locale_name is None:
        locale_name = negotiate_locale_name(request)
        request.locale_name = locale_name
    return locale_name
    request.  Deprecated in favor of using request.locale_name directly as of
    Pyramid 1.5."""
    return request.locale_name
def make_localizer(current_locale_name, translation_directories):
    """ Create a :class:`pyramid.i18n.Localizer` object
@@ -196,27 +194,10 @@
def get_localizer(request):
    """ Retrieve a :class:`pyramid.i18n.Localizer` object
    corresponding to the current request's locale name. """
    corresponding to the current request's locale name.  Deprecated in favor
    of using the ``request.localizer`` attribute directly as of Pyramid 1.5"""
    return request.localizer
    # no locale object cached on request
    try:
        registry = request.registry
    except AttributeError:
        registry = get_current_registry()
    current_locale_name = get_locale_name(request)
    localizer = registry.queryUtility(ILocalizer, name=current_locale_name)
    if localizer is None:
        # no localizer utility registered yet
        tdirs = registry.queryUtility(ITranslationDirectories, default=[])
        localizer = make_localizer(current_locale_name, tdirs)
        registry.registerUtility(localizer, ILocalizer,
                                 name=current_locale_name)
    return localizer
class Translations(gettext.GNUTranslations, object):
    """An extended translation catalog class (ripped off from Babel) """
@@ -359,3 +340,28 @@
            return self._domains.get(domain, self).ungettext(
                singular, plural, num)
class LocalizerRequestMixin(object):
    @reify
    def localizer(self):
        """ Convenience property to return a localizer """
        registry = self.registry
        current_locale_name = self.locale_name
        localizer = registry.queryUtility(ILocalizer, name=current_locale_name)
        if localizer is None:
            # no localizer utility registered yet
            tdirs = registry.queryUtility(ITranslationDirectories, default=[])
            localizer = make_localizer(current_locale_name, tdirs)
            registry.registerUtility(localizer, ILocalizer,
                                     name=current_locale_name)
        return localizer
    @reify
    def locale_name(self):
        locale_name = negotiate_locale_name(self)
        return locale_name
pyramid/request.py
@@ -24,7 +24,7 @@
    )
from pyramid.decorator import reify
from pyramid.i18n import get_localizer
from pyramid.i18n import LocalizerRequestMixin
from pyramid.response import Response
from pyramid.url import URLMethodsMixin
from pyramid.util import InstancePropertyMixin
@@ -303,7 +303,8 @@
@implementer(IRequest)
class Request(BaseRequest, DeprecatedRequestMethodsMixin, URLMethodsMixin,
              CallbackMethodsMixin, InstancePropertyMixin):
              CallbackMethodsMixin, InstancePropertyMixin,
              LocalizerRequestMixin):
    """
    A subclass of the :term:`WebOb` Request class.  An instance of
    this class is created by the :term:`router` and is provided to a
@@ -384,13 +385,7 @@
    def json_body(self):
        return json.loads(text_(self.body, self.charset))
    @reify
    def localizer(self):
        """ Convenience property to return a localizer by calling
            :func:`pyramid.i18n.get_localizer`. """
        return get_localizer(self)
def route_request_iface(name, bases=()):
    # zope.interface treats the __name__ as the __doc__ and changes __name__
    # to None for interfaces that contain spaces if you do not pass a
pyramid/testing.py
@@ -39,6 +39,7 @@
    CallbackMethodsMixin,
    )
from pyramid.i18n import LocalizerRequestMixin
from pyramid.url import URLMethodsMixin
from pyramid.util import InstancePropertyMixin
@@ -286,7 +287,8 @@
        
@implementer(IRequest)
class DummyRequest(DeprecatedRequestMethodsMixin, URLMethodsMixin,
                   CallbackMethodsMixin, InstancePropertyMixin):
                   CallbackMethodsMixin, InstancePropertyMixin,
                   LocalizerRequestMixin):
    """ A DummyRequest object (incompletely) imitates a :term:`request` object.
    The ``params``, ``environ``, ``headers``, ``path``, and
pyramid/tests/test_config/test_i18n.py
@@ -86,8 +86,10 @@
    def test_add_translation_dirs_registers_chameleon_translate(self):
        from pyramid.interfaces import IChameleonTranslate
        from pyramid.threadlocal import manager
        request = DummyRequest()
        from pyramid.request import Request
        config = self._makeOne(autocommit=True)
        request = Request.blank('/')
        request.registry = config.registry
        manager.push({'request':request, 'registry':config.registry})
        try:
            config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale')
@@ -103,12 +105,3 @@
        self.assertEqual(config.registry.getUtility(ITranslationDirectories),
                         [locale])
class DummyRequest:
    subpath = ()
    matchdict = None
    def __init__(self, environ=None):
        if environ is None:
            environ = {}
        self.environ = environ
        self.params = {}
        self.cookies = {}
pyramid/tests/test_i18n.py
@@ -6,7 +6,7 @@
localedir = os.path.join(here, 'pkgs', 'localeapp', 'locale')
import unittest
from pyramid.testing import cleanUp
from pyramid import testing
class TestTranslationString(unittest.TestCase):
    def _makeOne(self, *arg, **kw):
@@ -84,10 +84,10 @@
class Test_negotiate_locale_name(unittest.TestCase):
    def setUp(self):
        cleanUp()
        testing.setUp()
    def tearDown(self):
        cleanUp()
        testing.tearDown()
    def _callFUT(self, request):
        from pyramid.i18n import negotiate_locale_name
@@ -140,20 +140,14 @@
class Test_get_locale_name(unittest.TestCase):
    def setUp(self):
        cleanUp()
        testing.setUp()
    def tearDown(self):
        cleanUp()
        testing.tearDown()
    def _callFUT(self, request):
        from pyramid.i18n import get_locale_name
        return get_locale_name(request)
    def _registerImpl(self, impl):
        from pyramid.threadlocal import get_current_registry
        registry = get_current_registry()
        from pyramid.interfaces import ILocaleNegotiator
        registry.registerUtility(impl, ILocaleNegotiator)
    def test_name_on_request(self):
        request = DummyRequest()
@@ -161,19 +155,12 @@
        result = self._callFUT(request)
        self.assertEqual(result, 'ie')
    def test_name_not_on_request(self):
        self._registerImpl(dummy_negotiator)
        request = DummyRequest()
        result = self._callFUT(request)
        self.assertEqual(result, 'bogus')
        self.assertEqual(request.locale_name, 'bogus')
class Test_make_localizer(unittest.TestCase):
    def setUp(self):
        cleanUp()
        testing.setUp()
    def tearDown(self):
        cleanUp()
        testing.tearDown()
    def _callFUT(self, locale, tdirs):
        from pyramid.i18n import make_localizer
@@ -221,97 +208,26 @@
class Test_get_localizer(unittest.TestCase):
    def setUp(self):
        cleanUp()
        testing.setUp()
    def tearDown(self):
        cleanUp()
        testing.tearDown()
    def _callFUT(self, request):
        from pyramid.i18n import get_localizer
        return get_localizer(request)
    def test_default_localizer(self):
        # `get_localizer` returns a default localizer for `en`
        from pyramid.i18n import Localizer
    def test_it(self):
        request = DummyRequest()
        result = self._callFUT(request)
        self.assertEqual(result.__class__, Localizer)
        self.assertEqual(result.locale_name, 'en')
    def test_custom_localizer_for_default_locale(self):
        from pyramid.threadlocal import get_current_registry
        from pyramid.interfaces import ILocalizer
        registry = get_current_registry()
        dummy = object()
        registry.registerUtility(dummy, ILocalizer, name='en')
        request = DummyRequest()
        result = self._callFUT(request)
        self.assertEqual(result, dummy)
    def test_custom_localizer_for_custom_locale(self):
        from pyramid.threadlocal import get_current_registry
        from pyramid.interfaces import ILocalizer
        registry = get_current_registry()
        dummy = object()
        registry.registerUtility(dummy, ILocalizer, name='ie')
        request = DummyRequest()
        request.locale_name = 'ie'
        result = self._callFUT(request)
        self.assertEqual(result, dummy)
    def test_localizer_from_mo(self):
        from pyramid.threadlocal import get_current_registry
        from pyramid.interfaces import ITranslationDirectories
        from pyramid.i18n import Localizer
        registry = get_current_registry()
        localedirs = [localedir]
        registry.registerUtility(localedirs, ITranslationDirectories)
        request = DummyRequest()
        request.locale_name = 'de'
        result = self._callFUT(request)
        self.assertEqual(result.__class__, Localizer)
        self.assertEqual(result.translate('Approve', 'deformsite'),
                         'Genehmigen')
        self.assertEqual(result.translate('Approve'), 'Approve')
        self.assertTrue(hasattr(result, 'pluralize'))
    def test_localizer_from_mo_bad_mo(self):
        from pyramid.threadlocal import get_current_registry
        from pyramid.interfaces import ITranslationDirectories
        from pyramid.i18n import Localizer
        registry = get_current_registry()
        localedirs = [localedir]
        registry.registerUtility(localedirs, ITranslationDirectories)
        request = DummyRequest()
        request.locale_name = 'be'
        result = self._callFUT(request)
        self.assertEqual(result.__class__, Localizer)
        self.assertEqual(result.translate('Approve', 'deformsite'),
                         'Approve')
    def test_request_has_localizer(self):
        from pyramid.threadlocal import get_current_registry
        from pyramid.interfaces import ILocalizer
        from pyramid.request import Request
        # register mock localizer
        dummy = object()
        registry = get_current_registry()
        registry.registerUtility(dummy, ILocalizer, name='en')
        request = Request(environ={})
        self.assertEqual(request.localizer, dummy)
        # `get_localizer` is only called once...
        other = object()
        registry.registerUtility(other, ILocalizer, name='en')
        self.assertNotEqual(request.localizer, other)
        self.assertEqual(request.localizer, dummy)
        request.localizer = 'localizer'
        self.assertEqual(self._callFUT(request), 'localizer')
class Test_default_locale_negotiator(unittest.TestCase):
    def setUp(self):
        cleanUp()
        testing.setUp()
    def tearDown(self):
        cleanUp()
        testing.tearDown()
    def _callFUT(self, request):
        from pyramid.i18n import default_locale_negotiator
@@ -477,6 +393,70 @@
        result = t.dungettext('messages', 'foo1', 'foos1', 2)
        self.assertEqual(result, 'foos1')
class TestLocalizerRequestMixin(unittest.TestCase):
    def setUp(self):
        self.config = testing.setUp()
    def tearDown(self):
        testing.tearDown()
    def _makeOne(self):
        from pyramid.i18n import LocalizerRequestMixin
        request = LocalizerRequestMixin()
        request.registry = self.config.registry
        request.cookies = {}
        request.params = {}
        return request
    def test_default_localizer(self):
        # `localizer` returns a default localizer for `en`
        from pyramid.i18n import Localizer
        request = self._makeOne()
        self.assertEqual(request.localizer.__class__, Localizer)
        self.assertEqual(request.locale_name, 'en')
    def test_custom_localizer_for_default_locale(self):
        from pyramid.interfaces import ILocalizer
        dummy = object()
        self.config.registry.registerUtility(dummy, ILocalizer, name='en')
        request = self._makeOne()
        self.assertEqual(request.localizer, dummy)
    def test_custom_localizer_for_custom_locale(self):
        from pyramid.interfaces import ILocalizer
        dummy = object()
        self.config.registry.registerUtility(dummy, ILocalizer, name='ie')
        request = self._makeOne()
        request._LOCALE_ = 'ie'
        self.assertEqual(request.localizer, dummy)
    def test_localizer_from_mo(self):
        from pyramid.interfaces import ITranslationDirectories
        from pyramid.i18n import Localizer
        localedirs = [localedir]
        self.config.registry.registerUtility(
            localedirs, ITranslationDirectories)
        request = self._makeOne()
        request._LOCALE_ = 'de'
        result = request.localizer
        self.assertEqual(result.__class__, Localizer)
        self.assertEqual(result.translate('Approve', 'deformsite'),
                         'Genehmigen')
        self.assertEqual(result.translate('Approve'), 'Approve')
        self.assertTrue(hasattr(result, 'pluralize'))
    def test_localizer_from_mo_bad_mo(self):
        from pyramid.interfaces import ITranslationDirectories
        from pyramid.i18n import Localizer
        localedirs = [localedir]
        self.config.registry.registerUtility(
            localedirs, ITranslationDirectories)
        request = self._makeOne()
        request._LOCALE_ = 'be'
        result = request.localizer
        self.assertEqual(result.__class__, Localizer)
        self.assertEqual(result.translate('Approve', 'deformsite'),
                         'Approve')
class DummyRequest(object):
    def __init__(self):