Skip to content

Fix LazyString corruption on copy, deepcopy and pickle under Python 3.11+ - #131

Open
DerevenetsArtyom wants to merge 1 commit into
transifex:develfrom
DerevenetsArtyom:fix-lazystring-getstate-py311
Open

Fix LazyString corruption on copy, deepcopy and pickle under Python 3.11+#131
DerevenetsArtyom wants to merge 1 commit into
transifex:develfrom
DerevenetsArtyom:fix-lazystring-getstate-py311

Conversation

@DerevenetsArtyom

Copy link
Copy Markdown

Fixes #130.

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 rather than being the first of several.

Test

tests/common/test_strings.py::TestLazyString::test_copy_deepcopy_and_pickle covers all three operations. It fails without the one-line change, with the same TypeError: 'str' object is not callable raised from strings.py, and passes with it.

Ran locally using the CI images:

  • PYTHON_VERSION=3.12 DJANGO_VERSION=4.2 — 263 passed
  • PYTHON_VERSION=3.9 DJANGO_VERSION=3.2 — 263 passed

Note on make code_quality

flake8 reports transifex/common/strings.py:137:24: B023 Function definition does not bind loop variable 'func' on this branch. That warning is pre-existing on devel — it appears at line 136 there, and this change shifts it by one line — and it refers to the intentional double-wrapping in lazy_str_meta. Because code_quality lints only changed files, touching this file brings it into scope.

I left it untouched to keep the PR to a single concern, but I am happy to add a # noqa: B023 if you would rather see the check green.

….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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LazyString is corrupted by copy, deepcopy and pickle on Python 3.11+

1 participant