Skip to content

Commit 7ab27c5

Browse files
committed
Start on test module about Git.USE_SHELL and Git attributes
1 parent dffa930 commit 7ab27c5

File tree

1 file changed

+168
-0
lines changed

1 file changed

+168
-0
lines changed

test/deprecation/test_cmd_git.py

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""Tests for static and dynamic characteristics of Git class and instance attributes.
2+
3+
Currently this all relates to the deprecated :class:`Git.USE_SHELL` class attribute,
4+
which can also be accessed through instances. Some tests directly verify its behavior,
5+
including deprecation warnings, while others verify that other aspects of attribute
6+
access are not inadvertently broken by mechanisms introduced to issue the warnings.
7+
"""
8+
9+
import contextlib
10+
import sys
11+
from typing import Generator
12+
13+
if sys.version_info >= (3, 11):
14+
from typing import assert_type
15+
else:
16+
from typing_extensions import assert_type
17+
18+
import pytest
19+
20+
from git.cmd import Git
21+
22+
_USE_SHELL_DEPRECATED_FRAGMENT = "Git.USE_SHELL is deprecated"
23+
"""Text contained in all USE_SHELL deprecation warnings, and starting most of them."""
24+
25+
_USE_SHELL_DANGEROUS_FRAGMENT = "Setting Git.USE_SHELL to True is unsafe and insecure"
26+
"""Beginning text of USE_SHELL deprecation warnings when USE_SHELL is set True."""
27+
28+
29+
@pytest.fixture
30+
def reset_backing_attribute() -> Generator[None, None, None]:
31+
"""Fixture to reset the private ``_USE_SHELL`` attribute.
32+
33+
This is used to decrease the likelihood of state changes leaking out and affecting
34+
other tests. But the goal is not to assert that ``_USE_SHELL`` is used, nor anything
35+
about how or when it is used, which is an implementation detail subject to change.
36+
37+
This is possible but inelegant to do with pytest's monkeypatch fixture, which only
38+
restores attributes that it has previously been used to change, create, or remove.
39+
"""
40+
no_value = object()
41+
try:
42+
old_value = Git._USE_SHELL
43+
except AttributeError:
44+
old_value = no_value
45+
46+
yield
47+
48+
if old_value is no_value:
49+
with contextlib.suppress(AttributeError):
50+
del Git._USE_SHELL
51+
else:
52+
Git._USE_SHELL = old_value
53+
54+
55+
def test_cannot_access_undefined_on_git_class() -> None:
56+
"""Accessing a bogus attribute on the Git class remains a dynamic and static error.
57+
58+
This differs from Git instances, where most attribute names will dynamically
59+
synthesize a "bound method" that runs a git subcommand when called.
60+
"""
61+
with pytest.raises(AttributeError):
62+
Git.foo # type: ignore[attr-defined]
63+
64+
65+
def test_get_use_shell_on_class_default() -> None:
66+
"""USE_SHELL can be read as a class attribute, defaulting to False and warning."""
67+
with pytest.deprecated_call() as ctx:
68+
use_shell = Git.USE_SHELL
69+
70+
(message,) = [str(entry.message) for entry in ctx] # Exactly one warning.
71+
assert message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
72+
73+
assert_type(use_shell, bool)
74+
75+
# This comes after the static assertion, just in case it would affect the inference.
76+
assert not use_shell
77+
78+
79+
# FIXME: More robustly check that each operation really issues exactly one deprecation
80+
# warning, even if this requires relying more on reset_backing_attribute doing its job.
81+
def test_use_shell_on_class(reset_backing_attribute) -> None:
82+
"""USE_SHELL can be written and re-read as a class attribute, always warning."""
83+
# We assert in a "safe" order, using reset_backing_attribute only as a backstop.
84+
with pytest.deprecated_call() as ctx:
85+
Git.USE_SHELL = True
86+
set_value = Git.USE_SHELL
87+
Git.USE_SHELL = False
88+
reset_value = Git.USE_SHELL
89+
90+
# The attribute should take on the values set to it.
91+
assert set_value is True
92+
assert reset_value is False
93+
94+
messages = [str(entry.message) for entry in ctx]
95+
set_message, check_message, reset_message, recheck_message = messages
96+
97+
# Setting it to True should produce the special warning for that.
98+
assert _USE_SHELL_DEPRECATED_FRAGMENT in set_message
99+
assert set_message.startswith(_USE_SHELL_DANGEROUS_FRAGMENT)
100+
101+
# All other operations should produce a usual warning.
102+
assert check_message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
103+
assert reset_message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
104+
assert recheck_message.startswith(_USE_SHELL_DEPRECATED_FRAGMENT)
105+
106+
107+
# FIXME: Test behavior on instances (where we can get but not set).
108+
109+
# FIXME: Test behavior with multiprocessing (the attribute needs to pickle properly).
110+
111+
112+
_EXPECTED_DIR_SUBSET = {
113+
"cat_file_all",
114+
"cat_file_header",
115+
"GIT_PYTHON_TRACE",
116+
"USE_SHELL", # The attribute we get deprecation warnings for.
117+
"GIT_PYTHON_GIT_EXECUTABLE",
118+
"refresh",
119+
"is_cygwin",
120+
"polish_url",
121+
"check_unsafe_protocols",
122+
"check_unsafe_options",
123+
"AutoInterrupt",
124+
"CatFileContentStream",
125+
"__init__",
126+
"__getattr__",
127+
"set_persistent_git_options",
128+
"working_dir",
129+
"version_info",
130+
"execute",
131+
"environment",
132+
"update_environment",
133+
"custom_environment",
134+
"transform_kwarg",
135+
"transform_kwargs",
136+
"__call__",
137+
"_call_process", # Not currently considered public, but unlikely to change.
138+
"get_object_header",
139+
"get_object_data",
140+
"stream_object_data",
141+
"clear_cache",
142+
}
143+
"""Some stable attributes dir() should include on the Git class and its instances.
144+
145+
This is intentionally incomplete, but includes substantial variety. Most importantly, it
146+
includes both ``USE_SHELL`` and a wide sampling of other attributes.
147+
"""
148+
149+
150+
def test_class_dir() -> None:
151+
"""dir() on the Git class includes its statically known attributes.
152+
153+
This tests that the mechanism that adds dynamic behavior to USE_SHELL accesses so
154+
that all accesses issue warnings does not break dir() for the class, neither for
155+
USE_SHELL nor for ordinary (non-deprecated) attributes.
156+
"""
157+
actual = set(dir(Git))
158+
assert _EXPECTED_DIR_SUBSET <= actual
159+
160+
161+
def test_instance_dir() -> None:
162+
"""dir() on Git objects includes its statically known attributes.
163+
164+
This is like test_class_dir, but for Git instance rather than the class itself.
165+
"""
166+
instance = Git()
167+
actual = set(dir(instance))
168+
assert _EXPECTED_DIR_SUBSET <= actual

0 commit comments

Comments
 (0)