Repository: pallets/jinja (python)

Issue:
Cannot pickle `jinja2.utils.missing`
The `jinja2.utils.missing` singleton instance cannot be pickled since pickle cannot find `MissingType` due to the `jinja2.utils.MissingType` attribute not existing:

```python
import pickle
import jinja2.utils

pickle.dumps(jinja2.utils.missing)
```

Running the above code results in the following error:

```
Traceback (most recent call last):
  File "/tmp/bug.py", line 4, in <module>
    pickle.dumps(jinja2.utils.missing)
_pickle.PicklingError: Can't pickle <class 'jinja2.utils.MissingType'>: attribute lookup MissingType on jinja2.utils failed
```

Attempting to pickle (and then restore) the `jinja2.utils.missing` singleton instance should succeed. Supporting this would allow pickling of `jinja2.runtime.Undefined` instances which do not set `obj`, once https://github.com/pallets/jinja/issues/2025 is also resolved.

Environment:

- Python version: 3.11.1
- Jinja version: 3.1.4


Pull request description:
Fix pickle/copy support for the `missing` singleton
Fix pickle/copy support for the `missing` singleton:

* Fix pickle support for the `missing` singleton.
* Add unit test for pickling the `missing` singleton.
* Add unit test for copying the `missing` singleton.

Fixes #2027


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 58dc0321..1a1a526b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -24,6 +24,8 @@ Unreleased
     call. :issue:`2021`
 -   Fix dunder protocol (`copy`/`pickle`/etc) interaction with ``Undefined``
     objects. :issue:`2025`
+-   Fix `copy`/`pickle` support for the internal ``missing`` object.
+    :issue:`2027`
 
 
 Version 3.1.4
diff --git a/src/jinja2/utils.py b/src/jinja2/utils.py
index 5c1ff5d7..7b52fc03 100644
--- a/src/jinja2/utils.py
+++ b/src/jinja2/utils.py
@@ -18,8 +18,17 @@ if t.TYPE_CHECKING:
 
 F = t.TypeVar("F", bound=t.Callable[..., t.Any])
 
-# special singleton representing missing values for the runtime
-missing: t.Any = type("MissingType", (), {"__repr__": lambda x: "missing"})()
+
+class _MissingType:
+    def __repr__(self) -> str:
+        return "missing"
+
+    def __reduce__(self) -> str:
+        return "missing"
+
+
+missing: t.Any = _MissingType()
+"""Special singleton representing missing values for the runtime."""
 
 internal_code: t.MutableSet[CodeType] = set()
 


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 7b58af14..86e0f042 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,3 +1,4 @@
+import copy
 import pickle
 import random
 from collections import deque
@@ -183,3 +184,14 @@ def test_consume():
     consume(x)
     with pytest.raises(StopIteration):
         next(x)
+
+
+@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
+def test_pickle_missing(protocol: int) -> None:
+    """Test that missing can be pickled while remaining a singleton."""
+    assert pickle.loads(pickle.dumps(missing, protocol)) is missing
+
+
+def test_copy_missing() -> None:
+    """Test that missing can be copied while remaining a singleton."""
+    assert copy.copy(missing) is missing