1
+ import sys
2
+ import pytest
3
+ import warnings
4
+ from unittest import mock
5
+
6
+ # The module where the version check code resides
7
+ MODULE_PATH = "db_dtypes"
8
+ HELPER_MODULE_PATH = f"{ MODULE_PATH } ._versions_helpers"
9
+
10
+ @pytest .fixture (autouse = True )
11
+ def cleanup_imports ():
12
+ """
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
+
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
+ ]
115
+ original_modules = {}
116
+
117
+ # Store original modules and remove them
118
+ for mod_name in modules_to_clear :
119
+ original_modules [mod_name ] = sys .modules .get (mod_name )
120
+ if mod_name in sys .modules :
121
+ del sys .modules [mod_name ]
122
+
123
+ yield # Run the test
124
+
125
+ # Restore original modules after test
126
+ for mod_name , original_mod in original_modules .items ():
127
+ if original_mod :
128
+ 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
+
164
+ @pytest .mark .parametrize (
165
+ "patch_target_name" ,
166
+ [
167
+ "JSONArray" ,
168
+ "JSONDtype" ,
169
+ # Add both if needed, though one is sufficient to trigger the 'if'
170
+ # ("JSONArray", "JSONDtype"),
171
+ ]
172
+ )
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).
176
+ """
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__
0 commit comments