|
56 | 56 | import contextlib
|
57 | 57 | import sys
|
58 | 58 | from typing import Generator
|
| 59 | +import unittest.mock |
59 | 60 | import warnings
|
60 | 61 |
|
61 | 62 | if sys.version_info >= (3, 11):
|
@@ -250,6 +251,41 @@ def test_use_shell_cannot_set_on_instance(
|
250 | 251 | instance.USE_SHELL = value
|
251 | 252 |
|
252 | 253 |
|
| 254 | +@pytest.mark.filterwarnings("ignore::DeprecationWarning") |
| 255 | +@pytest.mark.parametrize("original_value", [False, True]) |
| 256 | +def test_use_shell_is_mock_patchable_on_class_as_object_attribute( |
| 257 | + original_value: bool, |
| 258 | + restore_use_shell_state: None, |
| 259 | +) -> None: |
| 260 | + """Asymmetric patching looking up USE_SHELL in ``__dict__`` doesn't corrupt state. |
| 261 | +
|
| 262 | + Code using GitPython may temporarily set Git.USE_SHELL to a different value. Ideally |
| 263 | + it does not use unittest.mock.patch to do so, because that makes subtle assumptions |
| 264 | + about the relationship between attributes and dictionaries. If the attribute can be |
| 265 | + retrieved from the ``__dict__`` rather than directly, that value is assumed the |
| 266 | + correct one to restore, even by a normal setattr. |
| 267 | +
|
| 268 | + The effect is that some ways of simulating a class attribute with added behavior can |
| 269 | + cause a descriptor, such as a property, to be set to its own backing attribute |
| 270 | + during unpatching; then subsequent reads raise RecursionError. This happens if both |
| 271 | + (a) setting it on the class is customized in a metaclass and (b) getting it on |
| 272 | + instances is customized with a descriptor (such as a property) in the class itself. |
| 273 | +
|
| 274 | + Although ideally code outside GitPython would not rely on being able to patch |
| 275 | + Git.USE_SHELL with unittest.mock.patch, the technique is widespread. Thus, USE_SHELL |
| 276 | + should be implemented in some way compatible with it. This test checks for that. |
| 277 | + """ |
| 278 | + Git.USE_SHELL = original_value |
| 279 | + if Git.USE_SHELL is not original_value: |
| 280 | + raise RuntimeError(f"Can't set up the test") |
| 281 | + new_value = not original_value |
| 282 | + |
| 283 | + with unittest.mock.patch.object(Git, "USE_SHELL", new_value): |
| 284 | + assert Git.USE_SHELL is new_value |
| 285 | + |
| 286 | + assert Git.USE_SHELL is original_value |
| 287 | + |
| 288 | + |
253 | 289 | _EXPECTED_DIR_SUBSET = {
|
254 | 290 | "cat_file_all",
|
255 | 291 | "cat_file_header",
|
|
0 commit comments