5454"""
5555
5656import contextlib
57+ import logging
5758import sys
5859from typing import Generator
5960import unittest .mock
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
8083def _suppress_deprecation_warning ()-> Generator [None ,None ,None ]:
@@ -85,37 +88,60 @@ def _suppress_deprecation_warning() -> Generator[None, None, None]:
8588
8689@pytest .fixture
8790def 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 """
97110no_value = object ()
98111
112+ # Try to save the original private state.
99113try :
100- old_backing_value = Git ._USE_SHELL
114+ old_private_value = Git ._USE_SHELL
101115except 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+
103125try :
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.
104129with _suppress_deprecation_warning ():
105130old_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.)
109134yield
110135
136+ # Try to restore the original public value.
111137with _suppress_deprecation_warning ():
112138Git .USE_SHELL = old_public_value
113139finally :
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
121147def 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 """
278304Git .USE_SHELL = original_value
279305if 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" )
281307new_value = not original_value
282308
283309with unittest .mock .patch .object (Git ,"USE_SHELL" ,new_value ):