Skip to content

Commit e725c82

Browse files
committed
Make the restore_use_shell_state fixture more robust
1 parent 6a35261 commit e725c82

File tree

1 file changed

+39
-13
lines changed

1 file changed

+39
-13
lines changed

test/deprecation/test_cmd_git.py

+39-13
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"""
5555

5656
import contextlib
57+
import logging
5758
import sys
5859
from typing import Generator
5960
import unittest.mock
@@ -75,6 +76,8 @@
7576
_USE_SHELL_DANGEROUS_FRAGMENT = "Setting Git.USE_SHELL to True is unsafe and insecure"
7677
"""Beginning text of USE_SHELL deprecation warnings when USE_SHELL is set True."""
7778

79+
_logger = logging.getLogger(__name__)
80+
7881

7982
@contextlib.contextmanager
8083
def _suppress_deprecation_warning() -> Generator[None, None, None]:
@@ -85,37 +88,60 @@ def _suppress_deprecation_warning() -> Generator[None, None, None]:
8588

8689
@pytest.fixture
8790
def restore_use_shell_state() -> Generator[None, None, None]:
88-
"""Fixture to attempt to restore state associated with the ``USE_SHELL`` attribute.
91+
"""Fixture to attempt to restore state associated with the USE_SHELL attribute.
8992
9093
This is used to decrease the likelihood of state changes leaking out and affecting
91-
other tests. But the goal is not to assert that ``_USE_SHELL`` is used, nor anything
92-
about how or when it is used, which is an implementation detail subject to change.
94+
other tests. But the goal is not to assert implementation details of USE_SHELL.
95+
96+
This covers two of the common implementation strategies, for convenience in testing
97+
both. USE_SHELL could be implemented in the metaclass:
9398
94-
This is possible but inelegant to do with pytest's monkeypatch fixture, which only
95-
restores attributes that it has previously been used to change, create, or remove.
99+
* With a separate _USE_SHELL backing attribute. If using a property or other
100+
descriptor, this is the natural way to do it, but custom __getattribute__ and
101+
__setattr__ logic, if it does more than adding warnings, may also use that.
102+
* Like a simple attribute, using USE_SHELL itself, stored as usual in the class
103+
dictionary, with custom __getattribute__/__setattr__ logic only to warn.
104+
105+
This tries to save private state, tries to save the public attribute value, yields
106+
to the test case, tries to restore the public attribute value, then tries to restore
107+
private state. The idea is that if the getting or setting logic is wrong in the code
108+
under test, the state will still most likely be reset successfully.
96109
"""
97110
no_value = object()
98111

112+
# Try to save the original private state.
99113
try:
100-
old_backing_value = Git._USE_SHELL
114+
old_private_value = Git._USE_SHELL
101115
except AttributeError:
102-
old_backing_value = no_value
116+
separate_backing_attribute = False
117+
try:
118+
old_private_value = type.__getattribute__(Git, "USE_SHELL")
119+
except AttributeError:
120+
old_private_value = no_value
121+
_logger.error("Cannot retrieve old private _USE_SHELL or USE_SHELL value")
122+
else:
123+
separate_backing_attribute = True
124+
103125
try:
126+
# Try to save the original public value. Rather than attempt to restore a state
127+
# where the attribute is not set, if we cannot do this we allow AttributeError
128+
# to propagate out of the fixture, erroring the test case before its code runs.
104129
with _suppress_deprecation_warning():
105130
old_public_value = Git.USE_SHELL
106131

107132
# This doesn't have its own try-finally because pytest catches exceptions raised
108133
# during the yield. (The outer try-finally catches exceptions in this fixture.)
109134
yield
110135

136+
# Try to restore the original public value.
111137
with _suppress_deprecation_warning():
112138
Git.USE_SHELL = old_public_value
113139
finally:
114-
if old_backing_value is no_value:
115-
with contextlib.suppress(AttributeError):
116-
del Git._USE_SHELL
117-
else:
118-
Git._USE_SHELL = old_backing_value
140+
# Try to restore the original private state.
141+
if separate_backing_attribute:
142+
Git._USE_SHELL = old_private_value
143+
elif old_private_value is not no_value:
144+
type.__setattr__(Git, "USE_SHELL", old_private_value)
119145

120146

121147
def test_cannot_access_undefined_on_git_class() -> None:
@@ -277,7 +303,7 @@ def test_use_shell_is_mock_patchable_on_class_as_object_attribute(
277303
"""
278304
Git.USE_SHELL = original_value
279305
if Git.USE_SHELL is not original_value:
280-
raise RuntimeError(f"Can't set up the test")
306+
raise RuntimeError("Can't set up the test")
281307
new_value = not original_value
282308

283309
with unittest.mock.patch.object(Git, "USE_SHELL", new_value):

0 commit comments

Comments
 (0)