54
54
"""
55
55
56
56
import contextlib
57
+ import logging
57
58
import sys
58
59
from typing import Generator
59
60
import unittest .mock
75
76
_USE_SHELL_DANGEROUS_FRAGMENT = "Setting Git.USE_SHELL to True is unsafe and insecure"
76
77
"""Beginning text of USE_SHELL deprecation warnings when USE_SHELL is set True."""
77
78
79
+ _logger = logging .getLogger (__name__ )
80
+
78
81
79
82
@contextlib .contextmanager
80
83
def _suppress_deprecation_warning () -> Generator [None , None , None ]:
@@ -85,37 +88,60 @@ def _suppress_deprecation_warning() -> Generator[None, None, None]:
85
88
86
89
@pytest .fixture
87
90
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.
89
92
90
93
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:
93
98
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.
96
109
"""
97
110
no_value = object ()
98
111
112
+ # Try to save the original private state.
99
113
try :
100
- old_backing_value = Git ._USE_SHELL
114
+ old_private_value = Git ._USE_SHELL
101
115
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
+
103
125
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.
104
129
with _suppress_deprecation_warning ():
105
130
old_public_value = Git .USE_SHELL
106
131
107
132
# This doesn't have its own try-finally because pytest catches exceptions raised
108
133
# during the yield. (The outer try-finally catches exceptions in this fixture.)
109
134
yield
110
135
136
+ # Try to restore the original public value.
111
137
with _suppress_deprecation_warning ():
112
138
Git .USE_SHELL = old_public_value
113
139
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 )
119
145
120
146
121
147
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(
277
303
"""
278
304
Git .USE_SHELL = original_value
279
305
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" )
281
307
new_value = not original_value
282
308
283
309
with unittest .mock .patch .object (Git , "USE_SHELL" , new_value ):
0 commit comments