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