python-babel / flask-babel

i18n and l10n support for Flask based on Babel and pytz
https://python-babel.github.io/flask-babel/
Other
444 stars 159 forks source link

RuntimeError when unpickling LazyStrings #103

Closed MichiK closed 8 years ago

MichiK commented 8 years ago

I have an application where the composition of a string and display of the localized version to the user happen in different parts at different times and I use pickle to throw around objects between them. While being not the most elegant thing ever seen, that worked perfectly fine with Flask-Babel 0.9 and speaklater 1.3.

However, since Flask-Babel 0.10 with its integrated speaklater, unpickling a LazyString gives a RuntimeError: maximum recursion depth exceeded:

  File "/usr/local/lib/python3.4/dist-packages/flask_babel/speaklater.py", line 12, in __getattr__
    string = text_type(self)
  File "/usr/local/lib/python3.4/dist-packages/flask_babel/speaklater.py", line 18, in __str__
    return text_type(self._func(*self._args, **self._kwargs))
  File "/usr/local/lib/python3.4/dist-packages/flask_babel/speaklater.py", line 12, in __getattr__
    string = text_type(self)
  File "/usr/local/lib/python3.4/dist-packages/flask_babel/speaklater.py", line 18, in __str__
    return text_type(self._func(*self._args, **self._kwargs))
  File "/usr/local/lib/python3.4/dist-packages/flask_babel/speaklater.py", line 12, in __getattr__
    string = text_type(self)
[...]

This seems to happen when pickle upon unpickling tries to call __setstate__. This leads to __getattr__ being called which in turn calls __str__ but since the object is not yet unpickled, pickle tries to do that, leading to __getattr__ being called for __setstate__ which wants to call __str__...

A minimal example to reproduce the bug would be the following:

import pickle

def noop(arg, *args):
    return arg

from flask_babel import LazyString # Flask-Babel 0.11.1
from speaklater import _LazyString # speaklater 1.3

s1 = _LazyString(noop, "a", {})
s2 = LazyString(noop, "a", {})

s1 and s2 look okay:

>>> s1
l'a'
>>> str(s1)
'a'
>>> s2
<flask_babel.speaklater.LazyString object at 0x7f7f7566fe48>
>>> str(s2)
'a'

Pickling and unpickling s1 works as expected:

>>> p = pickle.dumps(s1)
>>> pickle.loads(p)
l'a'

However, trying the same thing with s2 fails:

>>> p = pickle.dumps(s2)
>>> pickle.loads(p)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.4/dist-packages/flask_babel/speaklater.py", line 12, in __getattr__
    string = text_type(self)
  File "/usr/local/lib/python3.4/dist-packages/flask_babel/speaklater.py", line 18, in __str__
    return text_type(self._func(*self._args, **self._kwargs))
[...]
RuntimeError: maximum recursion depth exceeded
MichiK commented 8 years ago

Now that the PR is merged, this can be closed, I think.

TkTech commented 8 years ago

Github tip, if you write "closes #103" in the commit, when it gets merged the issue will also be automatically closed.