question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Monkey patching frozen attrs instances

See original GitHub issue

What’s the problem this feature will solve?

It is generally good [citation needed] to use frozen attrs clases to avoid bugs related to unintended alterations of class instances.

To change frozen instances, there is the evolve() function which returns a new instances with the updated attribute(s).

However, in tests you sometimes may want to monkey patch a frozen object in place.

For example, with Typed Settings you usually end up with a frozen SETTINGS object (which is an instance of your Settings class). Other modules import this object directly:

from .settings import SETTINGS

def spam():
    print(SETTINGS.my_option)

which is more convenient than:

from . import settings

def spam():
    print(settings.SETTINGS.my_option)

Since using monkeypatch.setattr() would raise a FrozenError, you need to use evolve(). But this does not work very well because you create an enirely new instances and need to monkeypatch all modules that import SETTINGS.

Describe the solution you’d like

monkeypatch.setattr() automatically (or explicitly) handles frozen attrs instances and sets an attribute (which is technically possible, see below).

Alternative Solutions

I could bundle a stripped down version of pytests monkey patcher but that would be less convenient for users.

Additional context

You can set attributes in frozen instances using object.__setattr__(target, name, value).

The changes to pytest could look like this:

Implicit version
diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py
index 31f95a95a..e71d5ddf9 100644
--- a/src/_pytest/monkeypatch.py
+++ b/src/_pytest/monkeypatch.py
@@ -14,6 +14,8 @@ from typing import Tuple
 from typing import TypeVar
 from typing import Union
 
+from attr.exceptions import FrozenError
+
 from _pytest.compat import final
 from _pytest.fixtures import fixture
 from _pytest.warning_types import PytestWarning
@@ -221,7 +223,10 @@ class MonkeyPatch:
         if inspect.isclass(target):
             oldval = target.__dict__.get(name, notset)
         self._setattr.append((target, name, oldval))
-        setattr(target, name, value)
+        try:
+            setattr(target, name, value)
+        except FrozenError:
+            object.__setattr__(target, name, value)
 
     def delattr(
         self,
@@ -361,7 +366,10 @@ class MonkeyPatch:
         """
         for obj, name, value in reversed(self._setattr):
             if value is not notset:
-                setattr(obj, name, value)
+                try:
+                    setattr(obj, name, value)
+                except FrozenError:
+                    object.__setattr__(obj, name, value)
             else:
                 delattr(obj, name)
         self._setattr[:] = []
Explicit version
diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py
index 31f95a95a..39baa2828 100644
--- a/src/_pytest/monkeypatch.py
+++ b/src/_pytest/monkeypatch.py
@@ -14,6 +14,8 @@ from typing import Tuple
 from typing import TypeVar
 from typing import Union
 
+from attr.exceptions import FrozenError
+
 from _pytest.compat import final
 from _pytest.fixtures import fixture
 from _pytest.warning_types import PytestWarning
@@ -181,6 +183,7 @@ class MonkeyPatch:
         name: Union[object, str],
         value: object = notset,
         raising: bool = True,
+        frozen: bool = False,
     ) -> None:
         """Set attribute value on target, memorizing the old value.
 
@@ -221,7 +224,13 @@ class MonkeyPatch:
         if inspect.isclass(target):
             oldval = target.__dict__.get(name, notset)
         self._setattr.append((target, name, oldval))
-        setattr(target, name, value)
+        try:
+            setattr(target, name, value)
+        except FrozenError:
+            if frozen:
+                object.__setattr__(target, name, value)
+            else:
+                raise
 
     def delattr(
         self,
@@ -361,7 +370,12 @@ class MonkeyPatch:
         """
         for obj, name, value in reversed(self._setattr):
             if value is not notset:
-                setattr(obj, name, value)
+                try:
+                    setattr(obj, name, value)
+                except FrozenError:
+                    # If we did set an attribute on a frozen instance, we surely can undo
+                    # these changes. :)
+                    object.__setattr__(obj, name, value)
             else:
                 delattr(obj, name)
         self._setattr[:] = []

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:14 (14 by maintainers)

github_iconTop GitHub Comments

1reaction
hynekcommented, Dec 18, 2021

Feel free to steal the wrapper from structlog if you decide otherwise. 😉

1reaction
RonnyPfannschmidtcommented, Dec 16, 2021

@sscherfke i consider that considerably worse

Read more comments on GitHub >

github_iconTop Results From Across the Web

Monkey Patching an attribute within a class? - Stack Overflow
Is there a way to do this? You must either create instance of the original class and then rewrite the actual value of...
Read more >
Switching an attrs class's frozen status at runtime #527 - GitHub
What I'd want would be a magic attrs.freeze() / attrs.unfreeze() that recursively went through an attrs style hierarchy and either froze/unfroze ...
Read more >
Python Monkey Patching - Anuradha Chowdhary
Monkey Patching is simply the dynamic replacement of attributes at runtime. In Python, the term monkey patch refers to dynamic (or run-time) ...
Read more >
Monkey Patching and its consequences - Python for the Lab
Monkey patching is a technique that allows you to alter the behavior of objects at runtime. Even though it can be a very...
Read more >
Should I use Monkey Patching in JavaScript? - Fek.io
To add a Monkey Patch to our object we simply need to assign a new function to the add function on our instantiated...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found