From a38cdfed9bfa8d0e75af5424d2ebfb4ba48fcce9 Mon Sep 17 00:00:00 2001 From: DerevenetsArtyom Date: Wed, 5 Aug 2026 22:29:23 +0300 Subject: [PATCH] Fix LazyString corruption on copy, deepcopy and pickle under Python 3.11+ Python 3.11 added `object.__getstate__`, so `__getstate__` now appears in `dir(str)` and `lazy_str_meta` wraps it like any other `str` method. The wrapper reports the evaluated text as the object's serialization state, so `copy.copy`, `copy.deepcopy` and `pickle` each reconstruct a `LazyString` whose `_func` attribute is a plain string, and evaluating that copy raises `TypeError: 'str' object is not callable`. The exclusion set already protects the rest of the serialization protocol (`__reduce__`, `__reduce_ex__`, `__getnewargs__`), but it predates 3.11 and so could not list `__getstate__`. Adding it restores the inherited `object.__getstate__`. `__getstate__` is the only name added to `dir(str)` between 3.10 and 3.12, so this single entry covers the regression. This is straightforward to hit through Django: forms deepcopy their fields on every instantiation and `ChoiceField` deepcopies its choices, so a lazy string used as a choice label is corrupted on every request that builds such a form. --- tests/common/test_strings.py | 9 +++++++++ transifex/common/strings.py | 1 + 2 files changed, 10 insertions(+) diff --git a/tests/common/test_strings.py b/tests/common/test_strings.py index c6593fe..9892b1a 100644 --- a/tests/common/test_strings.py +++ b/tests/common/test_strings.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import copy +import pickle import sys import pytest @@ -40,6 +42,13 @@ def test_contains(self): assert "world" not in LazyString(str.upper, "hello world") assert "WORLD" in LazyString(str.upper, "hello world") + def test_copy_deepcopy_and_pickle(self): + string = LazyString(str.upper, "hello world") + + assert str(copy.copy(string)) == "HELLO WORLD" + assert str(copy.deepcopy(string)) == "HELLO WORLD" + assert str(pickle.loads(pickle.dumps(string))) == "HELLO WORLD" + def test_eq(self): assert LazyString(str.upper, "hello world") == "HELLO WORLD" assert "HELLO WORLD" == LazyString(str.upper, "hello world") diff --git a/transifex/common/strings.py b/transifex/common/strings.py index daa70a0..447212b 100644 --- a/transifex/common/strings.py +++ b/transifex/common/strings.py @@ -112,6 +112,7 @@ def lazy_str_meta(name, bases, dct): "__init__", "__doc__", "__reduce__", + "__getstate__", "__new__", "__str__", "__dir__",