Opened 3 years ago

Last modified 3 years ago

#32272 new Bug

gettext_lazy inconsistent error when nested

Reported by: John Bazik Owned by: nobody
Component: Internationalization Version: 3.1
Severity: Normal Keywords:
Cc: Claude Paroz Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

When gettext_lazy is called with a lazy object, it returns a nested lazy object which has inconsistent behavior, depending on whether USE_I18N is set or not.

>>> from django import __version__
>>> print(__version__)
3.1
>>> from django.utils.functional import lazy
>>> from django.utils.translation.trans_real import gettext as gettext_real
>>> from django.utils.translation.trans_null import gettext as gettext_noop
>>> gettext_lazy_real = lazy(gettext_real, str)
>>> gettext_lazy_noop = lazy(gettext_noop, str)
>>> r1 = gettext_lazy_real('Real')
>>> r2 = gettext_lazy_real(r1)
>>> n1 = gettext_lazy_noop('Noop')
>>> n2 = gettext_lazy_noop(n1)
>>> print(str(r1))
Real
>>> print(str(r2))
Real
>>> print(str(n1))
Noop
>>> print(str(n2))
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: __str__ returned non-string (type __proxy__)

I discovered this problem while working with version 2.2.

I've run across this twice now. The first time, I created a pull request for another project to avoid the nesting. However, now that I've seen it again, I think that because it works in the case of USE_I18N=True, it's easy for developers to miss the problem, especially so since testing USE_I18N has some quirks. Here's my pull request:

https://github.com/django-cms/django-cms/pull/6896

I think the best fix would be to disallow nesting by simply returning already-nested objects, but I'm not sure of all the subtleties in that code.

Change History (4)

comment:2 by Mariusz Felisiak, 3 years ago

Cc: Claude Paroz added

comment:3 by Carlton Gibson, 3 years ago

Component: UtilitiesInternationalization
Triage Stage: UnreviewedAccepted

I'll accept this, since there is a difference in behaviour.

Given the code in trans_null.py, I'm not quite sure how the issue arrises:

def gettext(message):
    return message


gettext_noop = gettext_lazy = _ = gettext

Here gettext_lazy (and _) don't actually use lazy, so I'm not 100% sure how the equivalent line from the report comes up:

gettext_lazy_noop = lazy(gettext_noop, str)

However, you've hit it in the wild, so I must presume it does 🤔

I wonder if forcing a string from gettext() in the the trans_null case makes sense?

diff --git a/django/utils/translation/trans_null.py b/django/utils/translation/trans_null.py
index a687572b69..f590ff23cd 100644
--- a/django/utils/translation/trans_null.py
+++ b/django/utils/translation/trans_null.py
@@ -6,7 +6,7 @@ from django.conf import settings
 
 
 def gettext(message):
-    return message
+    return str(message)
 
 
 gettext_noop = gettext_lazy = _ = gettext

Resolves the reported issue but I don't know if it would have other consequences.

comment:4 by John Bazik, 3 years ago

Although gettext_lazy is, as you mention, set in trans_null.py

gettext_noop = gettext_lazy = _ = gettext

It is also set in translation/__init__.py, and that's what gets imported.

_trans = Trans()


def gettext(message):
    return _trans.gettext(message)


gettext_lazy = lazy(gettext, str)

In class Trans, at the top of that file, _trans.gettext is set to trans_null.gettext, which is this

def gettext(message):
    return message

I skipped all that in my example to keep it simple. Coercing the message, as you suggest, looks like a good solution to me.

comment:5 by John Bazik, 3 years ago

Here is another solution.

If gettext_lazy was defined in trans_real.py (it is not now), then it could be set in translation/__init__.py like this

def gettext_lazy(message):
    return _trans.gettext_lazy(message)

Then lazy would never get called when it isn't needed.

That would require moving some or all of the lazy stuff from __init__.py to trans_real.py which is more work. And there's a lot going on in trans_real.py already.

Another possibility is to apply lazy in Trans.__getattr__, in __init__.py, where USE_I18N is tested.

Note: See TracTickets for help on using tickets.
Back to Top