Skip to content

Commit 1bbd5f7

Browse files
committed
updates python version checks to ensure coverage
1 parent 6a5d943 commit 1bbd5f7

File tree

2 files changed

+144
-137
lines changed

2 files changed

+144
-137
lines changed

db_dtypes/__init__.py

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -337,34 +337,55 @@ def __sub__(self, other):
337337

338338
return super().__sub__(other)
339339

340+
# Inside db_dtypes/__init__.py (TEMPORARY DEBUG)
341+
print(f"DEBUG: Inside __init__, JSONArray type: {type(JSONArray)}, value: {repr(JSONArray)}")
342+
print(f"DEBUG: Inside __init__, JSONDtype type: {type(JSONDtype)}, value: {repr(JSONDtype)}")
340343

341-
sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version()
342-
if sys_major == 3 and sys_minor in (7, 8):
343-
warnings.warn(
344-
"The python-bigquery library as well as the python-db-dtypes-pandas library no "
345-
"longer supports Python 3.7 and Python 3.8. "
346-
f"Your Python version is {sys_major}.{sys_minor}.{sys_micro}. We "
347-
"recommend that you update soon to ensure ongoing support. For "
348-
"more details, see: [Google Cloud Client Libraries Supported Python Versions policy](https://cloud.google.com/python/docs/supported-python-versions)",
349-
FutureWarning,
350-
)
351-
352-
if not JSONArray or not JSONDtype:
353-
__all__ = [
344+
def _determine_all(json_array_type, json_dtype_type):
345+
"""Determines the list for __all__ based on JSON type availability."""
346+
base_all = [
354347
"__version__",
355348
"DateArray",
356349
"DateDtype",
357350
"TimeArray",
358351
"TimeDtype",
359352
]
360-
else:
361-
__all__ = [
362-
"__version__",
363-
"DateArray",
364-
"DateDtype",
365-
"JSONDtype",
366-
"JSONArray",
367-
"JSONArrowType",
368-
"TimeArray",
369-
"TimeDtype",
370-
]
353+
# Check if both JSON types are available (truthy)
354+
if json_array_type and json_dtype_type:
355+
# print("DEBUG: Condition FALSE, including JSON in __all__") # Keep if needed
356+
return base_all + ["JSONDtype", "JSONArray", "JSONArrowType"]
357+
else:
358+
# print("DEBUG: Condition TRUE, excluding JSON from __all__") # Keep if needed
359+
return base_all
360+
361+
def _check_python_version():
362+
"""Checks the runtime Python version and issues a warning if needed."""
363+
sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version()
364+
if sys_major == 3 and sys_minor in (7, 8):
365+
warnings.warn(
366+
"The python-bigquery library as well as the python-db-dtypes-pandas library no "
367+
"longer supports Python 3.7 and Python 3.8. "
368+
f"Your Python version is {sys_major}.{sys_minor}.{sys_micro}. We "
369+
"recommend that you update soon to ensure ongoing support. For "
370+
"more details, see: [Google Cloud Client Libraries Supported Python Versions policy](https://cloud.google.com/python/docs/supported-python-versions)",
371+
FutureWarning,
372+
stacklevel=2, # Point warning to the caller of __init__
373+
)
374+
375+
376+
# sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version()
377+
# if sys_major == 3 and sys_minor in (7, 8):
378+
# warnings.warn(
379+
# "The python-bigquery library as well as the python-db-dtypes-pandas library no "
380+
# "longer supports Python 3.7 and Python 3.8. "
381+
# f"Your Python version is {sys_major}.{sys_minor}.{sys_micro}. We "
382+
# "recommend that you update soon to ensure ongoing support. For "
383+
# "more details, see: [Google Cloud Client Libraries Supported Python Versions policy](https://cloud.google.com/python/docs/supported-python-versions)",
384+
# FutureWarning,
385+
# )
386+
387+
# Perform the version check
388+
_check_python_version()
389+
390+
# Assign __all__ by calling the function
391+
__all__ = _determine_all(JSONArray, JSONDtype)

tests/unit/test__init__.py

Lines changed: 99 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -5,147 +5,133 @@
55
from unittest import mock
66
import pyarrow as pa
77

8-
# The module where the version check code resides
8+
# Module paths used for mocking
99
MODULE_PATH = "db_dtypes"
1010
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
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

Comments
 (0)