55from unittest import mock
66import pyarrow as pa
77
8- # The module where the version check code resides
8+ # Module paths used for mocking
99MODULE_PATH = "db_dtypes"
1010HELPER_MODULE_PATH = f"{ MODULE_PATH } ._versions_helpers"
11+ MOCK_EXTRACT_VERSION = f"{ HELPER_MODULE_PATH } .extract_runtime_version"
12+ MOCK_WARN = "warnings.warn" # Target the standard warnings module
1113
12- @pytest .fixture
13- def cleanup_imports ():
14- """Ensures the target module and its helper are removed from sys.modules
15- before each test, allowing for clean imports with patching.
14+ @pytest .mark .parametrize (
15+ "mock_version_tuple, version_str" ,
16+ [
17+ ((3 , 7 , 10 ), "3.7.10" ),
18+ ((3 , 7 , 0 ), "3.7.0" ),
19+ ((3 , 8 , 5 ), "3.8.5" ),
20+ ((3 , 8 , 12 ), "3.8.12" ),
21+ ]
22+ )
23+ def test_check_python_version_warns_on_unsupported (mock_version_tuple , version_str ):
1624 """
25+ Test that _check_python_version issues a FutureWarning for Python 3.7/3.8.
26+ """
27+ # Import the function under test directly
28+ from db_dtypes import _check_python_version
29+
30+ # Mock the helper function it calls and the warnings.warn function
31+ with mock .patch (MOCK_EXTRACT_VERSION , return_value = mock_version_tuple ), \
32+ mock .patch (MOCK_WARN ) as mock_warn_call :
33+
34+ _check_python_version () # Call the function
35+
36+ # Assert that warnings.warn was called exactly once
37+ mock_warn_call .assert_called_once ()
38+
39+ # Check the arguments passed to warnings.warn
40+ args , kwargs = mock_warn_call .call_args
41+ assert len (args ) >= 1 # Should have at least the message
42+ warning_message = args [0 ]
43+ warning_category = args [1 ] if len (args ) > 1 else kwargs .get ('category' )
44+
45+ # Verify message content and category
46+ assert "longer supports Python 3.7 and Python 3.8" in warning_message
47+ assert f"Your Python version is { version_str } " in warning_message
48+ assert "https://cloud.google.com/python/docs/supported-python-versions" in warning_message
49+ assert warning_category == FutureWarning
50+ # Optionally check stacklevel if important
51+ assert kwargs .get ('stacklevel' ) == 2
1752
18- # Store original modules that might exist
19- original_modules = {}
20- modules_to_clear = [MODULE_PATH , HELPER_MODULE_PATH ]
21- for mod_name in modules_to_clear :
22- if mod_name in sys .modules :
23- original_modules [mod_name ] = sys .modules [mod_name ]
24- del sys .modules [mod_name ]
25-
26- yield # Run the test
27-
28- # Clean up again and restore originals if they existed
29- for mod_name in modules_to_clear :
30- if mod_name in sys .modules :
31- del sys .modules [mod_name ] # Remove if test imported it
32- # Restore original modules
33- for mod_name , original_mod in original_modules .items ():
34- if original_mod :
35- sys .modules [mod_name ] = original_mod
3653
3754@pytest .mark .parametrize (
38- "mock_version_tuple, version_str, expect_warning " ,
55+ "mock_version_tuple" ,
3956 [
40- # Cases expected to warn
41- ((3 , 7 , 10 ), "3.7.10" , True ),
42- ((3 , 7 , 0 ), "3.7.0" , True ),
43- ((3 , 8 , 5 ), "3.8.5" , True ),
44- ((3 , 8 , 12 ), "3.8.12" , True ),
45- # Cases NOT expected to warn
46- ((3 , 9 , 1 ), "3.9.1" , False ),
47- ((3 , 10 , 0 ), "3.10.0" , False ),
48- ((3 , 11 , 2 ), "3.11.2" , False ),
49- ((3 , 12 , 0 ), "3.12.0" , False ),
57+ (3 , 9 , 1 ),
58+ (3 , 10 , 0 ),
59+ (3 , 11 , 2 ),
60+ (3 , 12 , 0 ),
61+ (4 , 0 , 0 ), # Future version
62+ (3 , 6 , 0 ), # Older unsupported, but not 3.7/3.8
5063 ]
5164)
52- def test_python_version_warning_on_import (mock_version_tuple , version_str , expect_warning , cleanup_imports ):
53- """Test that a FutureWarning is raised ONLY for Python 3.7 or 3.8 during import.
65+ def test_check_python_version_does_not_warn_on_supported (mock_version_tuple ):
5466 """
55-
56- # Create a mock function that returns the desired version tuple
57- mock_extract_func = mock .Mock (return_value = mock_version_tuple )
58-
59- # Create a mock module object for _versions_helpers
60- mock_helpers_module = types .ModuleType (HELPER_MODULE_PATH )
61- mock_helpers_module .extract_runtime_version = mock_extract_func
62-
63- # Use mock.patch.dict to temporarily replace the module in sys.modules
64- # This ensures that when db_dtypes.__init__ does `from . import _versions_helpers`,
65- # it gets our mock module.
66- with mock .patch .dict (sys .modules , {HELPER_MODULE_PATH : mock_helpers_module }):
67- if expect_warning :
68- with pytest .warns (FutureWarning ) as record :
69- # The import will now use the mocked _versions_helpers module
70- import db_dtypes
71-
72- assert len (record ) == 1
73- warning_message = str (record [0 ].message )
74- assert "longer supports Python 3.7 and Python 3.8" in warning_message
75- else :
76- with warnings .catch_warnings (record = True ) as record :
77- warnings .simplefilter ("always" )
78- # The import will now use the mocked _versions_helpers module
79- import db_dtypes
80-
81- found_warning = False
82- for w in record :
83- if (issubclass (w .category , FutureWarning ) and
84- "longer supports Python 3.7 and Python 3.8" in str (w .message )):
85- found_warning = True
86- break
87- assert not found_warning , (
88- f"Unexpected FutureWarning raised for Python version { version_str } "
89- )
90-
91- # --- Test Case 1: JSON types available ---
92-
93- @pytest .fixture
94- def cleanup_imports_for_all (request ):
95- """
96- Ensures the target module and its dependencies potentially affecting
97- __all__ are removed from sys.modules before and after each test,
98- allowing for clean imports with patching. Also handles PyArrow extension type registration.
67+ Test that _check_python_version does NOT issue a warning for other versions.
9968 """
69+ # Import the function under test directly
70+ from db_dtypes import _check_python_version
10071
101- # Modules that might be checked or imported in __init__
102- modules_to_clear = [
103- MODULE_PATH ,
104- f"{ MODULE_PATH } .core" ,
105- f"{ MODULE_PATH } .json" ,
106- f"{ MODULE_PATH } .version" ,
107- f"{ MODULE_PATH } ._versions_helpers" ,
108- ]
109- original_modules = {}
72+ # Mock the helper function it calls and the warnings.warn function
73+ with mock .patch (MOCK_EXTRACT_VERSION , return_value = mock_version_tuple ), \
74+ mock .patch (MOCK_WARN ) as mock_warn_call :
11075
111- # Store original modules and remove them
112- for mod_name in modules_to_clear :
113- original_modules [mod_name ] = sys .modules .get (mod_name )
114- if mod_name in sys .modules :
115- del sys .modules [mod_name ]
76+ _check_python_version () # Call the function
11677
117- yield # Run the test
78+ # Assert that warnings.warn was NOT called
79+ mock_warn_call .assert_not_called ()
11880
119- # Restore original modules after test
120- for mod_name , original_mod in original_modules .items ():
121- if original_mod :
122- sys .modules [mod_name ] = original_mod
123- elif mod_name in sys .modules :
124- # If it wasn't there before but is now, remove it
125- del sys .modules [mod_name ]
12681
127- def test_all_includes_json_when_available ( cleanup_imports_for_all ):
82+ def test_determine_all_includes_json_when_available ( ):
12883 """
129- Test that __all__ includes JSON types when JSONArray and JSONDtype are available .
84+ Test that _determine_all includes JSON types when both are truthy .
13085 """
86+ # Import the function directly for testing
87+ from db_dtypes import _determine_all
13188
132- # No patching needed for the 'else' block, assume normal import works
133- # and JSONArray/JSONDtype are truthy.
134- import db_dtypes
89+ # Simulate available types (can be any truthy object)
90+ mock_json_array = object ()
91+ mock_json_dtype = object ()
92+
93+ result = _determine_all (mock_json_array , mock_json_dtype )
13594
13695 expected_all = [
13796 "__version__" ,
13897 "DateArray" ,
13998 "DateDtype" ,
99+ "TimeArray" ,
100+ "TimeDtype" ,
140101 "JSONDtype" ,
141102 "JSONArray" ,
142103 "JSONArrowType" ,
104+ ]
105+ assert set (result ) == set (expected_all )
106+ assert "JSONDtype" in result
107+ assert "JSONArray" in result
108+ assert "JSONArrowType" in result
109+
110+ @pytest .mark .parametrize (
111+ "mock_array, mock_dtype" ,
112+ [
113+ (None , object ()), # JSONArray is None
114+ (object (), None ), # JSONDtype is None
115+ (None , None ), # Both are None
116+ ]
117+ )
118+ def test_determine_all_excludes_json_when_unavailable (mock_array , mock_dtype ):
119+ """
120+ Test that _determine_all excludes JSON types if either is falsy.
121+ """
122+ # Import the function directly for testing
123+ from db_dtypes import _determine_all
124+
125+ result = _determine_all (mock_array , mock_dtype )
126+
127+ expected_all = [
128+ "__version__" ,
129+ "DateArray" ,
130+ "DateDtype" ,
143131 "TimeArray" ,
144132 "TimeDtype" ,
145133 ]
146- # Use set comparison for order independence, as __all__ order isn't critical
147- assert set (db_dtypes .__all__ ) == set (expected_all )
148- # Explicitly check presence of JSON types
149- assert "JSONDtype" in db_dtypes .__all__
150- assert "JSONArray" in db_dtypes .__all__
151- assert "JSONArrowType" in db_dtypes .__all__
134+ assert set (result ) == set (expected_all )
135+ assert "JSONDtype" not in result
136+ assert "JSONArray" not in result
137+ assert "JSONArrowType" not in result
0 commit comments