11import sys
22import pytest
3+ import types
34import warnings
45from unittest import mock
56
910
1011@pytest .fixture (autouse = True )
1112def 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.
1215 """
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 } "
9816
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
11518 original_modules = {}
116-
117- # Store original modules and remove them
19+ modules_to_clear = [MODULE_PATH , HELPER_MODULE_PATH ]
11820 for mod_name in modules_to_clear :
119- original_modules [mod_name ] = sys .modules .get (mod_name )
12021 if mod_name in sys .modules :
22+ original_modules [mod_name ] = sys .modules [mod_name ]
12123 del sys .modules [mod_name ]
12224
12325 yield # Run the test
12426
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
12632 for mod_name , original_mod in original_modules .items ():
12733 if original_mod :
12834 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 ---
16335
16436@pytest .mark .parametrize (
165- "patch_target_name " ,
37+ "mock_version_tuple, version_str, expect_warning " ,
16638 [
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 ),
17149 ]
17250)
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.
17653 """
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