Opened 5 years ago
Last modified 6 months ago
#32272 assigned Bug
gettext_lazy inconsistent error when nested
| Reported by: | John Bazik | Owned by: | Ahmed Nassar |
|---|---|---|---|
| Component: | Internationalization | Version: | 3.1 |
| Severity: | Normal | Keywords: | |
| Cc: | Claude Paroz | Triage Stage: | Accepted |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | yes | Patch needs improvement: | yes |
| 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 (9)
comment:2 by , 5 years ago
| Cc: | added |
|---|
comment:3 by , 5 years ago
| Component: | Utilities → Internationalization |
|---|---|
| Triage Stage: | Unreviewed → Accepted |
comment:4 by , 5 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 , 5 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.
comment:6 by , 7 months ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
comment:7 by , 7 months ago
| Has patch: | set |
|---|
comment:8 by , 6 months ago
| Patch needs improvement: | set |
|---|
comment:9 by , 6 months ago
| Patch needs improvement: | unset |
|---|
comment:10 by , 6 months ago
| Needs tests: | set |
|---|---|
| Patch needs improvement: | set |
Ahmed, you know that any code change needs at least one test in Django, right?
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 = _ = gettextHere
gettext_lazy(and_) don't actually uselazy, so I'm not 100% sure how the equivalent line from the report comes up:However, you've hit it in the wild, so I must presume it does 🤔
I wonder if forcing a string from
gettext()in the thetrans_nullcase makes sense?Resolves the reported issue but I don't know if it would have other consequences.