1
1
import sys
2
2
import pytest
3
+ import types
3
4
import warnings
4
5
from unittest import mock
5
6
9
10
10
11
@pytest .fixture (autouse = True )
11
12
def cleanup_imports ():
13
+ """Ensures the target module and its helper are removed from sys.modules
14
+ before each test, allowing for clean imports with patching.
12
15
"""
13
- Ensures the target module and its helper are removed from sys.modules
14
- before and after each test, allowing for clean imports with patching.
15
- """
16
- # Store original sys.version_info if it's not already stored
17
- if not hasattr (cleanup_imports , 'original_version_info' ):
18
- cleanup_imports .original_version_info = sys .version_info
19
-
20
- # Remove modules before test
21
- if MODULE_PATH in sys .modules :
22
- del sys .modules [MODULE_PATH ]
23
- if HELPER_MODULE_PATH in sys .modules :
24
- del sys .modules [HELPER_MODULE_PATH ]
25
-
26
- yield # Run the test
27
-
28
- # Restore original sys.version_info after test
29
- sys .version_info = cleanup_imports .original_version_info
30
-
31
- # Remove modules after test
32
- if MODULE_PATH in sys .modules :
33
- del sys .modules [MODULE_PATH ]
34
- if HELPER_MODULE_PATH in sys .modules :
35
- del sys .modules [HELPER_MODULE_PATH ]
36
-
37
-
38
- @pytest .mark .parametrize (
39
- "mock_version_tuple, version_str" ,
40
- [
41
- ((3 , 7 , 10 ), "3.7.10" ),
42
- ((3 , 7 , 0 ), "3.7.0" ),
43
- ((3 , 8 , 5 ), "3.8.5" ),
44
- ((3 , 8 , 12 ), "3.8.12" ),
45
- ]
46
- )
47
- def test_python_3_7_or_3_8_warning_on_import (mock_version_tuple , version_str ):
48
- """Test that a FutureWarning is raised for Python 3.7 during import."""
49
- # Create a mock object mimicking sys.version_info attributes
50
- # Use spec=sys.version_info to ensure it has the right attributes if needed,
51
- # though just setting major/minor/micro is usually sufficient here.
52
- mock_version_info = mock .Mock (spec = sys .version_info ,
53
- major = mock_version_tuple [0 ],
54
- minor = mock_version_tuple [1 ],
55
- micro = mock_version_tuple [2 ])
56
-
57
- # Patch sys.version_info *before* importing db_dtypes
58
- with mock .patch ('sys.version_info' , mock_version_info ):
59
- # Use pytest.warns to catch the expected warning during import
60
- with pytest .warns (FutureWarning ) as record :
61
- # This import triggers __init__.py, which calls
62
- # _versions_helpers.extract_runtime_version, which reads
63
- # the *mocked* sys.version_info
64
- import db_dtypes
65
-
66
- # Assert that exactly one warning was recorded
67
- assert len (record ) == 1
68
- warning_message = str (record [0 ].message )
69
- # Assert the warning message content is correct
70
- assert "longer supports Python 3.7 and Python 3.8" in warning_message
71
-
72
- @pytest .mark .parametrize (
73
- "mock_version_tuple" ,
74
- [
75
- (3 , 9 , 1 ), # Supported
76
- (3 , 10 , 0 ), # Supported
77
- (3 , 11 , 2 ), # Supported
78
- (3 , 12 , 0 ), # Supported
79
- ]
80
- )
81
- def test_no_warning_for_other_versions_on_import (mock_version_tuple ):
82
- """Test that no FutureWarning is raised for other Python versions during import."""
83
- with mock .patch (f"{ MODULE_PATH } ._versions_helpers.extract_runtime_version" , return_value = mock_version_tuple ):
84
- # Use warnings.catch_warnings to check that NO relevant warning is raised
85
- with warnings .catch_warnings (record = True ) as record :
86
- warnings .simplefilter ("always" ) # Ensure warnings aren't filtered out by default config
87
- import db_dtypes # Import triggers the code
88
-
89
- # Assert that no FutureWarning matching the specific message was recorded
90
- found_warning = False
91
- for w in record :
92
- # Check for the specific warning we want to ensure is NOT present
93
- if (issubclass (w .category , FutureWarning ) and
94
- "longer supports Python 3.7 and Python 3.8" in str (w .message )):
95
- found_warning = True
96
- break
97
- assert not found_warning , f"Unexpected FutureWarning raised for Python version { mock_version_tuple } "
98
16
99
-
100
- @pytest .fixture
101
- def cleanup_imports_for_all (request ):
102
- """
103
- Ensures the target module and its dependencies potentially affecting
104
- __all__ are removed from sys.modules before and after each test,
105
- allowing for clean imports with patching.
106
- """
107
- # Modules that might be checked or imported in __init__
108
- modules_to_clear = [
109
- MODULE_PATH ,
110
- f"{ MODULE_PATH } .core" ,
111
- f"{ MODULE_PATH } .json" ,
112
- f"{ MODULE_PATH } .version" ,
113
- f"{ MODULE_PATH } ._versions_helpers" ,
114
- ]
17
+ # Store original modules that might exist
115
18
original_modules = {}
116
-
117
- # Store original modules and remove them
19
+ modules_to_clear = [MODULE_PATH , HELPER_MODULE_PATH ]
118
20
for mod_name in modules_to_clear :
119
- original_modules [mod_name ] = sys .modules .get (mod_name )
120
21
if mod_name in sys .modules :
22
+ original_modules [mod_name ] = sys .modules [mod_name ]
121
23
del sys .modules [mod_name ]
122
24
123
25
yield # Run the test
124
26
125
- # Restore original modules after test
27
+ # Clean up again and restore originals if they existed
28
+ for mod_name in modules_to_clear :
29
+ if mod_name in sys .modules :
30
+ del sys .modules [mod_name ] # Remove if test imported it
31
+ # Restore original modules
126
32
for mod_name , original_mod in original_modules .items ():
127
33
if original_mod :
128
34
sys .modules [mod_name ] = original_mod
129
- elif mod_name in sys .modules :
130
- # If it wasn't there before but is now, remove it
131
- del sys .modules [mod_name ]
132
-
133
-
134
- # --- Test Case 1: JSON types available ---
135
-
136
- def test_all_includes_json_when_available (cleanup_imports_for_all ):
137
- """
138
- Test that __all__ includes JSON types when JSONArray and JSONDtype are available.
139
- """
140
- # No patching needed for the 'else' block, assume normal import works
141
- # and JSONArray/JSONDtype are truthy.
142
- import db_dtypes
143
-
144
- expected_all = [
145
- "__version__" ,
146
- "DateArray" ,
147
- "DateDtype" ,
148
- "JSONDtype" ,
149
- "JSONArray" ,
150
- "JSONArrowType" ,
151
- "TimeArray" ,
152
- "TimeDtype" ,
153
- ]
154
- # Use set comparison for order independence, as __all__ order isn't critical
155
- assert set (db_dtypes .__all__ ) == set (expected_all )
156
- # Explicitly check presence of JSON types
157
- assert "JSONDtype" in db_dtypes .__all__
158
- assert "JSONArray" in db_dtypes .__all__
159
- assert "JSONArrowType" in db_dtypes .__all__
160
-
161
-
162
- # --- Test Case 2: JSON types unavailable ---
163
35
164
36
@pytest .mark .parametrize (
165
- "patch_target_name " ,
37
+ "mock_version_tuple, version_str, expect_warning " ,
166
38
[
167
- "JSONArray" ,
168
- "JSONDtype" ,
169
- # Add both if needed, though one is sufficient to trigger the 'if'
170
- # ("JSONArray", "JSONDtype"),
39
+ # Cases expected to warn
40
+ ((3 , 7 , 10 ), "3.7.10" , True ),
41
+ ((3 , 7 , 0 ), "3.7.0" , True ),
42
+ ((3 , 8 , 5 ), "3.8.5" , True ),
43
+ ((3 , 8 , 12 ), "3.8.12" , True ),
44
+ # Cases NOT expected to warn
45
+ ((3 , 9 , 1 ), "3.9.1" , False ),
46
+ ((3 , 10 , 0 ), "3.10.0" , False ),
47
+ ((3 , 11 , 2 ), "3.11.2" , False ),
48
+ ((3 , 12 , 0 ), "3.12.0" , False ),
171
49
]
172
50
)
173
- def test_all_excludes_json_when_unavailable (cleanup_imports_for_all , patch_target_name ):
174
- """
175
- Test that __all__ excludes JSON types when JSONArray or JSONDtype is unavailable (falsy).
51
+ def test_python_version_warning_on_import (mock_version_tuple , version_str , expect_warning ):
52
+ """Test that a FutureWarning is raised ONLY for Python 3.7 or 3.8 during import.
176
53
"""
177
- patch_path = f"{ MODULE_PATH } .{ patch_target_name } "
178
-
179
- # Patch one of the JSON types to be None *before* importing db_dtypes.
180
- # This simulates the condition `if not JSONArray or not JSONDtype:` being true.
181
- with mock .patch (patch_path , None ):
182
- # Need to ensure the json submodule itself is loaded if patching its contents
183
- # If the patch target is directly in __init__, this isn't needed.
184
- # Assuming JSONArray/JSONDtype are imported *into* __init__ from .json:
185
- try :
186
- import db_dtypes .json
187
- except ImportError :
188
- # Handle cases where the json module might genuinely be missing
189
- pass
190
-
191
- # Now import the main module, which will evaluate __all__
192
- import db_dtypes
193
-
194
- expected_all = [
195
- "__version__" ,
196
- "DateArray" ,
197
- "DateDtype" ,
198
- "TimeArray" ,
199
- "TimeDtype" ,
200
- ]
201
- # Use set comparison for order independence
202
- assert set (db_dtypes .__all__ ) == set (expected_all )
203
- # Explicitly check absence of JSON types
204
- assert "JSONDtype" not in db_dtypes .__all__
205
- assert "JSONArray" not in db_dtypes .__all__
206
- assert "JSONArrowType" not in db_dtypes .__all__
54
+
55
+ # Create a mock function that returns the desired version tuple
56
+ mock_extract_func = mock .Mock (return_value = mock_version_tuple )
57
+
58
+ # Create a mock module object for _versions_helpers
59
+ mock_helpers_module = types .ModuleType (HELPER_MODULE_PATH )
60
+ mock_helpers_module .extract_runtime_version = mock_extract_func
61
+
62
+ # Use mock.patch.dict to temporarily replace the module in sys.modules
63
+ # This ensures that when db_dtypes.__init__ does `from . import _versions_helpers`,
64
+ # it gets our mock module.
65
+ with mock .patch .dict (sys .modules , {HELPER_MODULE_PATH : mock_helpers_module }):
66
+ if expect_warning :
67
+ with pytest .warns (FutureWarning ) as record :
68
+ # The import will now use the mocked _versions_helpers module
69
+ import db_dtypes
70
+
71
+ assert len (record ) == 1
72
+ warning_message = str (record [0 ].message )
73
+ assert "longer supports Python 3.7 and Python 3.8" in warning_message
74
+ assert f"Your Python version is { version_str } " in warning_message
75
+ assert "https://cloud.google.com/python/docs/supported-python-versions" in warning_message
76
+ else :
77
+ with warnings .catch_warnings (record = True ) as record :
78
+ warnings .simplefilter ("always" )
79
+ # The import will now use the mocked _versions_helpers module
80
+ import db_dtypes
81
+
82
+ found_warning = False
83
+ for w in record :
84
+ if (issubclass (w .category , FutureWarning ) and
85
+ "longer supports Python 3.7 and Python 3.8" in str (w .message )):
86
+ found_warning = True
87
+ break
88
+ assert not found_warning , (
89
+ f"Unexpected FutureWarning raised for Python version { version_str } "
90
+ )
0 commit comments