5
5
from unittest import mock
6
6
import pyarrow as pa
7
7
8
- # The module where the version check code resides
8
+ # Module paths used for mocking
9
9
MODULE_PATH = "db_dtypes"
10
10
HELPER_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
11
13
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 ):
16
24
"""
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
17
52
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
36
53
37
54
@pytest .mark .parametrize (
38
- "mock_version_tuple, version_str, expect_warning " ,
55
+ "mock_version_tuple" ,
39
56
[
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
50
63
]
51
64
)
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 ):
54
66
"""
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.
99
68
"""
69
+ # Import the function under test directly
70
+ from db_dtypes import _check_python_version
100
71
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 :
110
75
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
116
77
117
- yield # Run the test
78
+ # Assert that warnings.warn was NOT called
79
+ mock_warn_call .assert_not_called ()
118
80
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 ]
126
81
127
- def test_all_includes_json_when_available ( cleanup_imports_for_all ):
82
+ def test_determine_all_includes_json_when_available ( ):
128
83
"""
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 .
130
85
"""
86
+ # Import the function directly for testing
87
+ from db_dtypes import _determine_all
131
88
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 )
135
94
136
95
expected_all = [
137
96
"__version__" ,
138
97
"DateArray" ,
139
98
"DateDtype" ,
99
+ "TimeArray" ,
100
+ "TimeDtype" ,
140
101
"JSONDtype" ,
141
102
"JSONArray" ,
142
103
"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" ,
143
131
"TimeArray" ,
144
132
"TimeDtype" ,
145
133
]
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