From 9b32164929bab9480084d997628d2d4d6c12c33d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 19:46:15 +0000 Subject: [PATCH 01/17] feat: Drop support for Python 3.7 and 3.8 Removes support for Python 3.7 and 3.8, establishing Python 3.9 as the new minimum supported version. This change involves: - Updating `python_requires` and classifiers in `setup.py`. - Modifying Python versions in `noxfile.py` (default, unit tests, system tests) and ensuring constraint file logic remains correct. - Updating the GitHub Actions workflow (`unittest.yml`) matrix, runner, and coverage job version. - Deleting constraint files for Python 3.7 and 3.8 (`testing/constraints-3.7.txt`, `testing/constraints-3.8.txt`). - Removing Kokoro sample configuration directories (`.kokoro/samples/python3.7/`, `.kokoro/samples/python3.8/`). - Updating supported version mentions in `README.rst`. - Removing 3.7 and 3.8 from the `ALL_VERSIONS` list in `samples/snippets/noxfile.py`. --- .github/workflows/unittest.yml | 10 +++--- .kokoro/samples/python3.7/common.cfg | 40 --------------------- .kokoro/samples/python3.7/continuous.cfg | 6 ---- .kokoro/samples/python3.7/periodic-head.cfg | 11 ------ .kokoro/samples/python3.7/periodic.cfg | 6 ---- .kokoro/samples/python3.7/presubmit.cfg | 6 ---- .kokoro/samples/python3.8/common.cfg | 40 --------------------- .kokoro/samples/python3.8/continuous.cfg | 6 ---- .kokoro/samples/python3.8/periodic-head.cfg | 11 ------ .kokoro/samples/python3.8/periodic.cfg | 6 ---- .kokoro/samples/python3.8/presubmit.cfg | 6 ---- README.rst | 4 +-- noxfile.py | 6 ++-- samples/snippets/noxfile.py | 2 +- setup.py | 4 +-- testing/constraints-3.7.txt | 10 ------ testing/constraints-3.8.txt | 2 -- 17 files changed, 10 insertions(+), 166 deletions(-) delete mode 100644 .kokoro/samples/python3.7/common.cfg delete mode 100644 .kokoro/samples/python3.7/continuous.cfg delete mode 100644 .kokoro/samples/python3.7/periodic-head.cfg delete mode 100644 .kokoro/samples/python3.7/periodic.cfg delete mode 100644 .kokoro/samples/python3.7/presubmit.cfg delete mode 100644 .kokoro/samples/python3.8/common.cfg delete mode 100644 .kokoro/samples/python3.8/continuous.cfg delete mode 100644 .kokoro/samples/python3.8/periodic-head.cfg delete mode 100644 .kokoro/samples/python3.8/periodic.cfg delete mode 100644 .kokoro/samples/python3.8/presubmit.cfg delete mode 100644 testing/constraints-3.7.txt delete mode 100644 testing/constraints-3.8.txt diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 699045c..41694c3 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -5,13 +5,11 @@ on: name: unittest jobs: unit: - # TODO(https://github.com/googleapis/gapic-generator-python/issues/2303): use `ubuntu-latest` once this bug is fixed. - # Use ubuntu-22.04 until Python 3.7 is removed from the test matrix - # https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories - runs-on: ubuntu-22.04 + # Use `ubuntu-latest` runner. + runs-on: ubuntu-latest strategy: matrix: - python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - name: Checkout uses: actions/checkout@v4 @@ -103,7 +101,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - name: Install coverage run: | python -m pip install --upgrade setuptools pip wheel diff --git a/.kokoro/samples/python3.7/common.cfg b/.kokoro/samples/python3.7/common.cfg deleted file mode 100644 index cf54acc..0000000 --- a/.kokoro/samples/python3.7/common.cfg +++ /dev/null @@ -1,40 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Specify which tests to run -env_vars: { - key: "RUN_TESTS_SESSION" - value: "py-3.7" -} - -# Declare build specific Cloud project. -env_vars: { - key: "BUILD_SPECIFIC_GCLOUD_PROJECT" - value: "python-docs-samples-tests-py37" -} - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-db-dtypes-pandas/.kokoro/test-samples.sh" -} - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" -} - -# Download secrets for samples -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "python-db-dtypes-pandas/.kokoro/trampoline_v2.sh" \ No newline at end of file diff --git a/.kokoro/samples/python3.7/continuous.cfg b/.kokoro/samples/python3.7/continuous.cfg deleted file mode 100644 index a1c8d97..0000000 --- a/.kokoro/samples/python3.7/continuous.cfg +++ /dev/null @@ -1,6 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -env_vars: { - key: "INSTALL_LIBRARY_FROM_SOURCE" - value: "True" -} \ No newline at end of file diff --git a/.kokoro/samples/python3.7/periodic-head.cfg b/.kokoro/samples/python3.7/periodic-head.cfg deleted file mode 100644 index ee3d564..0000000 --- a/.kokoro/samples/python3.7/periodic-head.cfg +++ /dev/null @@ -1,11 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -env_vars: { - key: "INSTALL_LIBRARY_FROM_SOURCE" - value: "True" -} - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-db-dtypes-pandas/.kokoro/test-samples-against-head.sh" -} diff --git a/.kokoro/samples/python3.7/periodic.cfg b/.kokoro/samples/python3.7/periodic.cfg deleted file mode 100644 index 71cd1e5..0000000 --- a/.kokoro/samples/python3.7/periodic.cfg +++ /dev/null @@ -1,6 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -env_vars: { - key: "INSTALL_LIBRARY_FROM_SOURCE" - value: "False" -} diff --git a/.kokoro/samples/python3.7/presubmit.cfg b/.kokoro/samples/python3.7/presubmit.cfg deleted file mode 100644 index a1c8d97..0000000 --- a/.kokoro/samples/python3.7/presubmit.cfg +++ /dev/null @@ -1,6 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -env_vars: { - key: "INSTALL_LIBRARY_FROM_SOURCE" - value: "True" -} \ No newline at end of file diff --git a/.kokoro/samples/python3.8/common.cfg b/.kokoro/samples/python3.8/common.cfg deleted file mode 100644 index a8500a8..0000000 --- a/.kokoro/samples/python3.8/common.cfg +++ /dev/null @@ -1,40 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Specify which tests to run -env_vars: { - key: "RUN_TESTS_SESSION" - value: "py-3.8" -} - -# Declare build specific Cloud project. -env_vars: { - key: "BUILD_SPECIFIC_GCLOUD_PROJECT" - value: "python-docs-samples-tests-py38" -} - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-db-dtypes-pandas/.kokoro/test-samples.sh" -} - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" -} - -# Download secrets for samples -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "python-db-dtypes-pandas/.kokoro/trampoline_v2.sh" \ No newline at end of file diff --git a/.kokoro/samples/python3.8/continuous.cfg b/.kokoro/samples/python3.8/continuous.cfg deleted file mode 100644 index a1c8d97..0000000 --- a/.kokoro/samples/python3.8/continuous.cfg +++ /dev/null @@ -1,6 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -env_vars: { - key: "INSTALL_LIBRARY_FROM_SOURCE" - value: "True" -} \ No newline at end of file diff --git a/.kokoro/samples/python3.8/periodic-head.cfg b/.kokoro/samples/python3.8/periodic-head.cfg deleted file mode 100644 index ee3d564..0000000 --- a/.kokoro/samples/python3.8/periodic-head.cfg +++ /dev/null @@ -1,11 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -env_vars: { - key: "INSTALL_LIBRARY_FROM_SOURCE" - value: "True" -} - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-db-dtypes-pandas/.kokoro/test-samples-against-head.sh" -} diff --git a/.kokoro/samples/python3.8/periodic.cfg b/.kokoro/samples/python3.8/periodic.cfg deleted file mode 100644 index 71cd1e5..0000000 --- a/.kokoro/samples/python3.8/periodic.cfg +++ /dev/null @@ -1,6 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -env_vars: { - key: "INSTALL_LIBRARY_FROM_SOURCE" - value: "False" -} diff --git a/.kokoro/samples/python3.8/presubmit.cfg b/.kokoro/samples/python3.8/presubmit.cfg deleted file mode 100644 index a1c8d97..0000000 --- a/.kokoro/samples/python3.8/presubmit.cfg +++ /dev/null @@ -1,6 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -env_vars: { - key: "INSTALL_LIBRARY_FROM_SOURCE" - value: "True" -} \ No newline at end of file diff --git a/README.rst b/README.rst index abf1e87..eab2705 100644 --- a/README.rst +++ b/README.rst @@ -34,11 +34,11 @@ dependencies. Supported Python Versions ^^^^^^^^^^^^^^^^^^^^^^^^^ -Python >= 3.7 +Python >= 3.9 Unsupported Python Versions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Python <= 3.6. +Python <= 3.8. Mac/Linux diff --git a/noxfile.py b/noxfile.py index 363fc2e..5df6e2e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -32,11 +32,9 @@ ISORT_VERSION = "isort==5.11.0" LINT_PATHS = ["docs", "db_dtypes", "tests", "noxfile.py", "setup.py"] -DEFAULT_PYTHON_VERSION = "3.8" +DEFAULT_PYTHON_VERSION = "3.9" UNIT_TEST_PYTHON_VERSIONS: List[str] = [ - "3.7", - "3.8", "3.9", "3.10", "3.11", @@ -56,7 +54,7 @@ UNIT_TEST_EXTRAS: List[str] = [] UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} -SYSTEM_TEST_PYTHON_VERSIONS: List[str] = ["3.8"] +SYSTEM_TEST_PYTHON_VERSIONS: List[str] = ["3.9"] SYSTEM_TEST_STANDARD_DEPENDENCIES: List[str] = [ "mock", "pytest", diff --git a/samples/snippets/noxfile.py b/samples/snippets/noxfile.py index c9a3d1e..23b5403 100644 --- a/samples/snippets/noxfile.py +++ b/samples/snippets/noxfile.py @@ -89,7 +89,7 @@ def get_pytest_env_vars() -> Dict[str, str]: # DO NOT EDIT - automatically generated. # All versions used to test samples. -ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] +ALL_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13"] # Any default versions that should be ignored. IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] diff --git a/setup.py b/setup.py index 98bed9d..10111b5 100644 --- a/setup.py +++ b/setup.py @@ -63,8 +63,6 @@ def readme(): "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -75,6 +73,6 @@ def readme(): ], platforms="Posix; MacOS X; Windows", install_requires=dependencies, - python_requires=">=3.7", + python_requires=">=3.9", tests_require=["pytest"], ) diff --git a/testing/constraints-3.7.txt b/testing/constraints-3.7.txt deleted file mode 100644 index a5c7a03..0000000 --- a/testing/constraints-3.7.txt +++ /dev/null @@ -1,10 +0,0 @@ -# This constraints file is used to check that lower bounds -# are correct in setup.py -# List *all* library dependencies and extras in this file. -# Pin the version to the lower bound. -# -# e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev", -packaging==17.0 -pandas==1.2.0 -pyarrow==3.0.0 -numpy==1.16.6 diff --git a/testing/constraints-3.8.txt b/testing/constraints-3.8.txt deleted file mode 100644 index 2e7f354..0000000 --- a/testing/constraints-3.8.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Make sure we test with pandas 1.2.0. The Python version isn't that relevant. -pandas==1.2.0 From dccef483c9502c8e08f2b1a2dbeda14754ef8701 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 18 Apr 2025 09:46:07 +0000 Subject: [PATCH 02/17] Updates python version in lint.yml --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4866193..35b2ef7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel From f81c738e5cb7a79e29c9eefc2f86024094617dfb Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 18 Apr 2025 10:16:46 +0000 Subject: [PATCH 03/17] Updates owlbot, removing reference to 3.8 --- owlbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owlbot.py b/owlbot.py index 18bd623..04664d8 100644 --- a/owlbot.py +++ b/owlbot.py @@ -28,7 +28,7 @@ # Add templated files # ---------------------------------------------------------------------------- templated_files = common.py_library( - system_test_python_versions=["3.8"], + system_test_python_versions=["3.9"], cov_level=100, intersphinx_dependencies={ "pandas": "https://pandas.pydata.org/pandas-docs/stable/" From 5cb30ca0177fba176eeb7b2a08524837f92bccc5 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 18 Apr 2025 10:32:04 +0000 Subject: [PATCH 04/17] Updates CONTRIBUTING.rst --- CONTRIBUTING.rst | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 0bda74a..c71f9d5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -22,7 +22,7 @@ In order to add a feature: documentation. - The feature must work fully on the following CPython versions: - 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13 on both UNIX and Windows. + 3.9, 3.10, 3.11, 3.12 and 3.13 on both UNIX and Windows. - The feature must not add unnecessary dependencies (where "unnecessary" is of course subjective, but new dependencies should @@ -143,12 +143,12 @@ Running System Tests $ nox -s system # Run a single system test - $ nox -s system-3.8 -- -k + $ nox -s system-3.9 -- -k .. note:: - System tests are only configured to run under Python 3.8. + System tests are only configured to run under Python 3.9. For expediency, we do not run them in older versions of Python 3. This alone will not run the tests. You'll need to change some local @@ -195,11 +195,11 @@ configure them just like the System Tests. # Run all tests in a folder $ cd samples/snippets - $ nox -s py-3.8 + $ nox -s py-3.9 # Run a single sample test $ cd samples/snippets - $ nox -s py-3.8 -- -k + $ nox -s py-3.9 -- -k ******************************************** Note About ``README`` as it pertains to PyPI @@ -221,16 +221,12 @@ Supported Python Versions We support: -- `Python 3.7`_ -- `Python 3.8`_ - `Python 3.9`_ - `Python 3.10`_ - `Python 3.11`_ - `Python 3.12`_ - `Python 3.13`_ -.. _Python 3.7: https://docs.python.org/3.7/ -.. _Python 3.8: https://docs.python.org/3.8/ .. _Python 3.9: https://docs.python.org/3.9/ .. _Python 3.10: https://docs.python.org/3.10/ .. _Python 3.11: https://docs.python.org/3.11/ From 7f4442455612094bfcc78663ed4bfcedb3059dac Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 18 Apr 2025 10:41:27 +0000 Subject: [PATCH 05/17] updates pytest warnings --- pytest.ini | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pytest.ini b/pytest.ini index c58342d..dbe13ba 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,13 +2,6 @@ filterwarnings = # treat all warnings as errors error - # Remove once support for python 3.7 and 3.8 is dropped - # Ignore warnings from older versions of pandas which still have python 3.7/3.8 support - ignore:.*distutils Version classes are deprecated:DeprecationWarning - ignore:.*resolve package from __spec__ or __package__, falling back on __name__ and __path__:ImportWarning - # Remove once https://github.com/dateutil/dateutil/issues/1314 is fixed - # dateutil is a dependency of pandas - ignore:datetime.datetime.utcfromtimestamp\(\) is deprecated:DeprecationWarning:dateutil.tz.tz # Remove once https://github.com/googleapis/python-db-dtypes-pandas/issues/227 is fixed ignore:.*any.*with datetime64 dtypes is deprecated and will raise in a future version:FutureWarning ignore:.*all.*with datetime64 dtypes is deprecated and will raise in a future version:FutureWarning From 3e4e6f6a197461c5a1567efbea66b72b60036906 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 18 Apr 2025 10:43:54 +0000 Subject: [PATCH 06/17] Removes test_samples-impl ref to older virtualenv package --- .kokoro/test-samples-impl.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.kokoro/test-samples-impl.sh b/.kokoro/test-samples-impl.sh index 53e365b..40e2488 100755 --- a/.kokoro/test-samples-impl.sh +++ b/.kokoro/test-samples-impl.sh @@ -33,8 +33,7 @@ export PYTHONUNBUFFERED=1 env | grep KOKORO # Install nox -# `virtualenv==20.26.6` is added for Python 3.7 compatibility -python3.9 -m pip install --upgrade --quiet nox virtualenv==20.26.6 +python3.9 -m pip install --upgrade --quiet nox virtualenv # Use secrets acessor service account to get secrets if [[ -f "${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" ]]; then From 7beea0d2f66f1543c694d760cb811d59867af9e4 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 18 Apr 2025 11:41:12 +0000 Subject: [PATCH 07/17] Removes references to pandas older than 1.5.0 --- db_dtypes/__init__.py | 10 +---- db_dtypes/core.py | 2 - .../time/test_time_compliance_1_5.py | 2 +- tests/unit/test_dtypes.py | 40 +++++++++---------- 4 files changed, 20 insertions(+), 34 deletions(-) diff --git a/db_dtypes/__init__.py b/db_dtypes/__init__.py index 2424ff4..5a83f60 100644 --- a/db_dtypes/__init__.py +++ b/db_dtypes/__init__.py @@ -46,15 +46,7 @@ # nanosecond precision when boxing scalars. _NP_BOX_DTYPE = "datetime64[us]" - -# To use JSONArray and JSONDtype, you'll need Pandas 1.5.0 or later. With the removal -# of Python 3.7 compatibility, the minimum Pandas version will be updated to 1.5.0. -if packaging.version.Version(pandas.__version__) >= packaging.version.Version("1.5.0"): - from db_dtypes.json import JSONArray, JSONArrowType, JSONDtype -else: - JSONArray = None - JSONDtype = None - +from db_dtypes.json import JSONArray, JSONArrowType, JSONDtype @pandas.api.extensions.register_extension_dtype class TimeDtype(core.BaseDatetimeDtype): diff --git a/db_dtypes/core.py b/db_dtypes/core.py index 7c9eb6b..695c108 100644 --- a/db_dtypes/core.py +++ b/db_dtypes/core.py @@ -186,8 +186,6 @@ def median( keepdims: bool = False, skipna: bool = True, ): - if not hasattr(pandas_backports, "numpy_validate_median"): - raise NotImplementedError("Need pandas 1.3 or later to calculate median.") pandas_backports.numpy_validate_median( (), diff --git a/tests/compliance/time/test_time_compliance_1_5.py b/tests/compliance/time/test_time_compliance_1_5.py index e8f2c93..6723443 100644 --- a/tests/compliance/time/test_time_compliance_1_5.py +++ b/tests/compliance/time/test_time_compliance_1_5.py @@ -24,7 +24,7 @@ import pytest # NDArrayBacked2DTests suite added in https://github.com/pandas-dev/pandas/pull/44974 -pytest.importorskip("pandas", minversion="1.5.0dev") +pytest.importorskip("pandas") class Test2DCompat(base.NDArrayBacked2DTests): diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py index 87b6a92..f3c9021 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/test_dtypes.py @@ -20,8 +20,6 @@ pd = pytest.importorskip("pandas") np = pytest.importorskip("numpy") -pandas_release = packaging.version.parse(pd.__version__).release - SAMPLE_RAW_VALUES = dict( dbdate=(datetime.date(2021, 2, 2), "2021-2-3", pd.NaT), dbtime=(datetime.time(1, 2, 2), "1:2:3.5", pd.NaT), @@ -538,39 +536,37 @@ def test_min_max_median(dtype): a = cls(data) assert a.min() == sample_values[0] assert a.max() == sample_values[-1] - if pandas_release >= (1, 3): - assert ( - a.median() == datetime.time(1, 2, 4) - if dtype == "dbtime" - else datetime.date(2021, 2, 3) - ) + + assert ( + a.median() == datetime.time(1, 2, 4) + if dtype == "dbtime" + else datetime.date(2021, 2, 3) + ) empty = cls([]) assert empty.min() is pd.NaT assert empty.max() is pd.NaT - if pandas_release >= (1, 3): - assert empty.median() is pd.NaT + assert empty.median() is pd.NaT empty = cls([None]) assert empty.min() is pd.NaT assert empty.max() is pd.NaT assert empty.min(skipna=False) is pd.NaT assert empty.max(skipna=False) is pd.NaT - if pandas_release >= (1, 3): - with pytest.warns(RuntimeWarning, match="empty slice"): - # It's weird that we get the warning here, and not - # below. :/ - assert empty.median() is pd.NaT - assert empty.median(skipna=False) is pd.NaT + + with pytest.warns(RuntimeWarning, match="empty slice"): + # It's weird that we get the warning here, and not + # below. :/ + assert empty.median() is pd.NaT + assert empty.median(skipna=False) is pd.NaT a = _make_one(dtype) assert a.min() == sample_values[0] assert a.max() == sample_values[1] - if pandas_release >= (1, 3): - assert ( - a.median() == datetime.time(1, 2, 2, 750000) - if dtype == "dbtime" - else datetime.date(2021, 2, 2) - ) + assert ( + a.median() == datetime.time(1, 2, 2, 750000) + if dtype == "dbtime" + else datetime.date(2021, 2, 2) + ) def test_date_add(): From 32d7b8f60777f5167f64edeb96899bdb1f4be0e9 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 18 Apr 2025 12:42:24 +0000 Subject: [PATCH 08/17] Removes pandas older than 1.5 and misc changes --- db_dtypes/__init__.py | 10 ++++------ db_dtypes/core.py | 1 - samples/snippets/requirements-test.txt | 3 +-- scripts/readme-gen/templates/install_deps.tmpl.rst | 2 +- tests/unit/test_dtypes.py | 3 +-- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/db_dtypes/__init__.py b/db_dtypes/__init__.py index 5a83f60..5cd49da 100644 --- a/db_dtypes/__init__.py +++ b/db_dtypes/__init__.py @@ -21,7 +21,6 @@ import warnings import numpy -import packaging.version import pandas import pandas.api.extensions from pandas.errors import OutOfBoundsDatetime @@ -29,6 +28,7 @@ import pyarrow.compute from db_dtypes import core +from db_dtypes.json import JSONArray, JSONArrowType, JSONDtype from db_dtypes.version import __version__ from . import _versions_helpers @@ -46,7 +46,6 @@ # nanosecond precision when boxing scalars. _NP_BOX_DTYPE = "datetime64[us]" -from db_dtypes.json import JSONArray, JSONArrowType, JSONDtype @pandas.api.extensions.register_extension_dtype class TimeDtype(core.BaseDatetimeDtype): @@ -342,15 +341,14 @@ def __sub__(self, other): sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version() if sys_major == 3 and sys_minor in (7, 8): warnings.warn( - "The python-bigquery library will stop supporting Python 3.7 " - "and Python 3.8 in a future major release expected in Q4 2024. " + "The python-bigquery library as well as the python-db-dtypes-pandas library no " + "longer supports Python 3.7 and Python 3.8. " f"Your Python version is {sys_major}.{sys_minor}.{sys_micro}. We " "recommend that you update soon to ensure ongoing support. For " "more details, see: [Google Cloud Client Libraries Supported Python Versions policy](https://cloud.google.com/python/docs/supported-python-versions)", - PendingDeprecationWarning, + FutureWarning, ) - if not JSONArray or not JSONDtype: __all__ = [ "__version__", diff --git a/db_dtypes/core.py b/db_dtypes/core.py index 695c108..a82edd1 100644 --- a/db_dtypes/core.py +++ b/db_dtypes/core.py @@ -186,7 +186,6 @@ def median( keepdims: bool = False, skipna: bool = True, ): - pandas_backports.numpy_validate_median( (), {"out": out, "overwrite_input": overwrite_input, "keepdims": keepdims}, diff --git a/samples/snippets/requirements-test.txt b/samples/snippets/requirements-test.txt index 57b712f..2c78728 100644 --- a/samples/snippets/requirements-test.txt +++ b/samples/snippets/requirements-test.txt @@ -1,2 +1 @@ -pytest===7.4.4; python_version == '3.7' # prevents dependabot from upgrading it -pytest==8.3.3; python_version > '3.7' +pytest==8.3.5 diff --git a/scripts/readme-gen/templates/install_deps.tmpl.rst b/scripts/readme-gen/templates/install_deps.tmpl.rst index 6f069c6..4a7f648 100644 --- a/scripts/readme-gen/templates/install_deps.tmpl.rst +++ b/scripts/readme-gen/templates/install_deps.tmpl.rst @@ -12,7 +12,7 @@ Install Dependencies .. _Python Development Environment Setup Guide: https://cloud.google.com/python/setup -#. Create a virtualenv. Samples are compatible with Python 3.7+. +#. Create a virtualenv. Samples are compatible with Python >= 3.9. .. code-block:: bash diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py index f3c9021..381a580 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/test_dtypes.py @@ -14,7 +14,6 @@ import datetime -import packaging.version import pytest pd = pytest.importorskip("pandas") @@ -536,7 +535,7 @@ def test_min_max_median(dtype): a = cls(data) assert a.min() == sample_values[0] assert a.max() == sample_values[-1] - + assert ( a.median() == datetime.time(1, 2, 4) if dtype == "dbtime" From e871d73a1d062617d0c4cb7921393ec1f1fcaefb Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 18 Apr 2025 12:53:00 +0000 Subject: [PATCH 09/17] updates pandas in setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 10111b5..587fee3 100644 --- a/setup.py +++ b/setup.py @@ -31,8 +31,8 @@ dependencies = [ "packaging >= 17.0", - "pandas >= 1.2.0", - "pyarrow>=3.0.0", + "pandas >= 1.5.0", + "pyarrow >= 3.0.0", "numpy >= 1.16.6", ] From 2bdf3ef6522cf3b0bcf1f4afd00dd9139a3fa40c Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 18 Apr 2025 13:37:32 +0000 Subject: [PATCH 10/17] more updates related to pandas --- db_dtypes/pandas_backports.py | 7 +++---- tests/compliance/date/test_date_compliance_1_5.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/db_dtypes/pandas_backports.py b/db_dtypes/pandas_backports.py index f8009ea..8f7800f 100644 --- a/db_dtypes/pandas_backports.py +++ b/db_dtypes/pandas_backports.py @@ -40,9 +40,8 @@ numpy_validate_max = pandas.compat.numpy.function.validate_max numpy_validate_min = pandas.compat.numpy.function.validate_min -if pandas_release >= (1, 3): - nanmedian = pandas.core.nanops.nanmedian - numpy_validate_median = pandas.compat.numpy.function.validate_median +nanmedian = pandas.core.nanops.nanmedian +numpy_validate_median = pandas.compat.numpy.function.validate_median def import_default(module_name, force=False, default=None): @@ -82,7 +81,7 @@ def _cmp_method(self, other, op): # pragma: NO COVER # TODO: use public API once pandas 1.5 / 2.x is released. # See: https://github.com/pandas-dev/pandas/pull/45544 -@import_default("pandas.core.arrays._mixins", pandas_release < (1, 3)) +@import_default("pandas.core.arrays._mixins") class NDArrayBackedExtensionArray(pandas.core.arrays.base.ExtensionArray): def __init__(self, values, dtype): assert isinstance(values, numpy.ndarray) diff --git a/tests/compliance/date/test_date_compliance_1_5.py b/tests/compliance/date/test_date_compliance_1_5.py index e8f2c93..6723443 100644 --- a/tests/compliance/date/test_date_compliance_1_5.py +++ b/tests/compliance/date/test_date_compliance_1_5.py @@ -24,7 +24,7 @@ import pytest # NDArrayBacked2DTests suite added in https://github.com/pandas-dev/pandas/pull/44974 -pytest.importorskip("pandas", minversion="1.5.0dev") +pytest.importorskip("pandas") class Test2DCompat(base.NDArrayBacked2DTests): From d902e31c700936145a2fd130f809434e38648ef2 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 18 Apr 2025 16:45:25 +0000 Subject: [PATCH 11/17] still broken --- db_dtypes/__init__.py | 1 + tests/unit/test__init__.py | 206 +++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 tests/unit/test__init__.py diff --git a/db_dtypes/__init__.py b/db_dtypes/__init__.py index 5cd49da..5ff82ca 100644 --- a/db_dtypes/__init__.py +++ b/db_dtypes/__init__.py @@ -339,6 +339,7 @@ def __sub__(self, other): sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version() +print(f"DINOSAUR: {sys_major}.{sys_minor}.{sys_micro}") if sys_major == 3 and sys_minor in (7, 8): warnings.warn( "The python-bigquery library as well as the python-db-dtypes-pandas library no " diff --git a/tests/unit/test__init__.py b/tests/unit/test__init__.py new file mode 100644 index 0000000..8491459 --- /dev/null +++ b/tests/unit/test__init__.py @@ -0,0 +1,206 @@ +import sys +import pytest +import warnings +from unittest import mock + +# The module where the version check code resides +MODULE_PATH = "db_dtypes" +HELPER_MODULE_PATH = f"{MODULE_PATH}._versions_helpers" + +@pytest.fixture(autouse=True) +def cleanup_imports(): + """ + Ensures the target module and its helper are removed from sys.modules + before and after each test, allowing for clean imports with patching. + """ + # Store original sys.version_info if it's not already stored + if not hasattr(cleanup_imports, 'original_version_info'): + cleanup_imports.original_version_info = sys.version_info + + # Remove modules before test + if MODULE_PATH in sys.modules: + del sys.modules[MODULE_PATH] + if HELPER_MODULE_PATH in sys.modules: + del sys.modules[HELPER_MODULE_PATH] + + yield # Run the test + + # Restore original sys.version_info after test + sys.version_info = cleanup_imports.original_version_info + + # Remove modules after test + if MODULE_PATH in sys.modules: + del sys.modules[MODULE_PATH] + if HELPER_MODULE_PATH in sys.modules: + del sys.modules[HELPER_MODULE_PATH] + + +@pytest.mark.parametrize( + "mock_version_tuple, version_str", + [ + ((3, 7, 10), "3.7.10"), + ((3, 7, 0), "3.7.0"), + ((3, 8, 5), "3.8.5"), + ((3, 8, 12), "3.8.12"), + ] +) +def test_python_3_7_or_3_8_warning_on_import(mock_version_tuple, version_str): + """Test that a FutureWarning is raised for Python 3.7 during import.""" + # Create a mock object mimicking sys.version_info attributes + # Use spec=sys.version_info to ensure it has the right attributes if needed, + # though just setting major/minor/micro is usually sufficient here. + mock_version_info = mock.Mock(spec=sys.version_info, + major=mock_version_tuple[0], + minor=mock_version_tuple[1], + micro=mock_version_tuple[2]) + + # Patch sys.version_info *before* importing db_dtypes + with mock.patch('sys.version_info', mock_version_info): + # Use pytest.warns to catch the expected warning during import + with pytest.warns(FutureWarning) as record: + # This import triggers __init__.py, which calls + # _versions_helpers.extract_runtime_version, which reads + # the *mocked* sys.version_info + import db_dtypes + + # Assert that exactly one warning was recorded + assert len(record) == 1 + warning_message = str(record[0].message) + # Assert the warning message content is correct + assert "longer supports Python 3.7 and Python 3.8" in warning_message + +@pytest.mark.parametrize( + "mock_version_tuple", + [ + (3, 9, 1), # Supported + (3, 10, 0), # Supported + (3, 11, 2), # Supported + (3, 12, 0), # Supported + ] +) +def test_no_warning_for_other_versions_on_import(mock_version_tuple): + """Test that no FutureWarning is raised for other Python versions during import.""" + with mock.patch(f"{MODULE_PATH}._versions_helpers.extract_runtime_version", return_value=mock_version_tuple): + # Use warnings.catch_warnings to check that NO relevant warning is raised + with warnings.catch_warnings(record=True) as record: + warnings.simplefilter("always") # Ensure warnings aren't filtered out by default config + import db_dtypes # Import triggers the code + + # Assert that no FutureWarning matching the specific message was recorded + found_warning = False + for w in record: + # Check for the specific warning we want to ensure is NOT present + if (issubclass(w.category, FutureWarning) and + "longer supports Python 3.7 and Python 3.8" in str(w.message)): + found_warning = True + break + assert not found_warning, f"Unexpected FutureWarning raised for Python version {mock_version_tuple}" + + +@pytest.fixture +def cleanup_imports_for_all(request): + """ + Ensures the target module and its dependencies potentially affecting + __all__ are removed from sys.modules before and after each test, + allowing for clean imports with patching. + """ + # Modules that might be checked or imported in __init__ + modules_to_clear = [ + MODULE_PATH, + f"{MODULE_PATH}.core", + f"{MODULE_PATH}.json", + f"{MODULE_PATH}.version", + f"{MODULE_PATH}._versions_helpers", + ] + original_modules = {} + + # Store original modules and remove them + for mod_name in modules_to_clear: + original_modules[mod_name] = sys.modules.get(mod_name) + if mod_name in sys.modules: + del sys.modules[mod_name] + + yield # Run the test + + # Restore original modules after test + for mod_name, original_mod in original_modules.items(): + if original_mod: + sys.modules[mod_name] = original_mod + elif mod_name in sys.modules: + # If it wasn't there before but is now, remove it + del sys.modules[mod_name] + + +# --- Test Case 1: JSON types available --- + +def test_all_includes_json_when_available(cleanup_imports_for_all): + """ + Test that __all__ includes JSON types when JSONArray and JSONDtype are available. + """ + # No patching needed for the 'else' block, assume normal import works + # and JSONArray/JSONDtype are truthy. + import db_dtypes + + expected_all = [ + "__version__", + "DateArray", + "DateDtype", + "JSONDtype", + "JSONArray", + "JSONArrowType", + "TimeArray", + "TimeDtype", + ] + # Use set comparison for order independence, as __all__ order isn't critical + assert set(db_dtypes.__all__) == set(expected_all) + # Explicitly check presence of JSON types + assert "JSONDtype" in db_dtypes.__all__ + assert "JSONArray" in db_dtypes.__all__ + assert "JSONArrowType" in db_dtypes.__all__ + + +# --- Test Case 2: JSON types unavailable --- + +@pytest.mark.parametrize( + "patch_target_name", + [ + "JSONArray", + "JSONDtype", + # Add both if needed, though one is sufficient to trigger the 'if' + # ("JSONArray", "JSONDtype"), + ] +) +def test_all_excludes_json_when_unavailable(cleanup_imports_for_all, patch_target_name): + """ + Test that __all__ excludes JSON types when JSONArray or JSONDtype is unavailable (falsy). + """ + patch_path = f"{MODULE_PATH}.{patch_target_name}" + + # Patch one of the JSON types to be None *before* importing db_dtypes. + # This simulates the condition `if not JSONArray or not JSONDtype:` being true. + with mock.patch(patch_path, None): + # Need to ensure the json submodule itself is loaded if patching its contents + # If the patch target is directly in __init__, this isn't needed. + # Assuming JSONArray/JSONDtype are imported *into* __init__ from .json: + try: + import db_dtypes.json + except ImportError: + # Handle cases where the json module might genuinely be missing + pass + + # Now import the main module, which will evaluate __all__ + import db_dtypes + + expected_all = [ + "__version__", + "DateArray", + "DateDtype", + "TimeArray", + "TimeDtype", + ] + # Use set comparison for order independence + assert set(db_dtypes.__all__) == set(expected_all) + # Explicitly check absence of JSON types + assert "JSONDtype" not in db_dtypes.__all__ + assert "JSONArray" not in db_dtypes.__all__ + assert "JSONArrowType" not in db_dtypes.__all__ \ No newline at end of file From a0543b96e2fd7bf6c8484a6e794229bd1d6d6088 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Wed, 23 Apr 2025 13:54:00 +0000 Subject: [PATCH 12/17] Updates FutureWarning tests to account for unittest coverage --- db_dtypes/__init__.py | 1 - tests/unit/test__init__.py | 238 ++++++++++--------------------------- 2 files changed, 61 insertions(+), 178 deletions(-) diff --git a/db_dtypes/__init__.py b/db_dtypes/__init__.py index 5ff82ca..5cd49da 100644 --- a/db_dtypes/__init__.py +++ b/db_dtypes/__init__.py @@ -339,7 +339,6 @@ def __sub__(self, other): sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version() -print(f"DINOSAUR: {sys_major}.{sys_minor}.{sys_micro}") if sys_major == 3 and sys_minor in (7, 8): warnings.warn( "The python-bigquery library as well as the python-db-dtypes-pandas library no " diff --git a/tests/unit/test__init__.py b/tests/unit/test__init__.py index 8491459..8fb8a85 100644 --- a/tests/unit/test__init__.py +++ b/tests/unit/test__init__.py @@ -1,5 +1,6 @@ import sys import pytest +import types import warnings from unittest import mock @@ -9,198 +10,81 @@ @pytest.fixture(autouse=True) def cleanup_imports(): + """Ensures the target module and its helper are removed from sys.modules + before each test, allowing for clean imports with patching. """ - Ensures the target module and its helper are removed from sys.modules - before and after each test, allowing for clean imports with patching. - """ - # Store original sys.version_info if it's not already stored - if not hasattr(cleanup_imports, 'original_version_info'): - cleanup_imports.original_version_info = sys.version_info - - # Remove modules before test - if MODULE_PATH in sys.modules: - del sys.modules[MODULE_PATH] - if HELPER_MODULE_PATH in sys.modules: - del sys.modules[HELPER_MODULE_PATH] - - yield # Run the test - - # Restore original sys.version_info after test - sys.version_info = cleanup_imports.original_version_info - - # Remove modules after test - if MODULE_PATH in sys.modules: - del sys.modules[MODULE_PATH] - if HELPER_MODULE_PATH in sys.modules: - del sys.modules[HELPER_MODULE_PATH] - - -@pytest.mark.parametrize( - "mock_version_tuple, version_str", - [ - ((3, 7, 10), "3.7.10"), - ((3, 7, 0), "3.7.0"), - ((3, 8, 5), "3.8.5"), - ((3, 8, 12), "3.8.12"), - ] -) -def test_python_3_7_or_3_8_warning_on_import(mock_version_tuple, version_str): - """Test that a FutureWarning is raised for Python 3.7 during import.""" - # Create a mock object mimicking sys.version_info attributes - # Use spec=sys.version_info to ensure it has the right attributes if needed, - # though just setting major/minor/micro is usually sufficient here. - mock_version_info = mock.Mock(spec=sys.version_info, - major=mock_version_tuple[0], - minor=mock_version_tuple[1], - micro=mock_version_tuple[2]) - - # Patch sys.version_info *before* importing db_dtypes - with mock.patch('sys.version_info', mock_version_info): - # Use pytest.warns to catch the expected warning during import - with pytest.warns(FutureWarning) as record: - # This import triggers __init__.py, which calls - # _versions_helpers.extract_runtime_version, which reads - # the *mocked* sys.version_info - import db_dtypes - - # Assert that exactly one warning was recorded - assert len(record) == 1 - warning_message = str(record[0].message) - # Assert the warning message content is correct - assert "longer supports Python 3.7 and Python 3.8" in warning_message - -@pytest.mark.parametrize( - "mock_version_tuple", - [ - (3, 9, 1), # Supported - (3, 10, 0), # Supported - (3, 11, 2), # Supported - (3, 12, 0), # Supported - ] -) -def test_no_warning_for_other_versions_on_import(mock_version_tuple): - """Test that no FutureWarning is raised for other Python versions during import.""" - with mock.patch(f"{MODULE_PATH}._versions_helpers.extract_runtime_version", return_value=mock_version_tuple): - # Use warnings.catch_warnings to check that NO relevant warning is raised - with warnings.catch_warnings(record=True) as record: - warnings.simplefilter("always") # Ensure warnings aren't filtered out by default config - import db_dtypes # Import triggers the code - - # Assert that no FutureWarning matching the specific message was recorded - found_warning = False - for w in record: - # Check for the specific warning we want to ensure is NOT present - if (issubclass(w.category, FutureWarning) and - "longer supports Python 3.7 and Python 3.8" in str(w.message)): - found_warning = True - break - assert not found_warning, f"Unexpected FutureWarning raised for Python version {mock_version_tuple}" - -@pytest.fixture -def cleanup_imports_for_all(request): - """ - Ensures the target module and its dependencies potentially affecting - __all__ are removed from sys.modules before and after each test, - allowing for clean imports with patching. - """ - # Modules that might be checked or imported in __init__ - modules_to_clear = [ - MODULE_PATH, - f"{MODULE_PATH}.core", - f"{MODULE_PATH}.json", - f"{MODULE_PATH}.version", - f"{MODULE_PATH}._versions_helpers", - ] + # Store original modules that might exist original_modules = {} - - # Store original modules and remove them + modules_to_clear = [MODULE_PATH, HELPER_MODULE_PATH] for mod_name in modules_to_clear: - original_modules[mod_name] = sys.modules.get(mod_name) if mod_name in sys.modules: + original_modules[mod_name] = sys.modules[mod_name] del sys.modules[mod_name] yield # Run the test - # Restore original modules after test + # Clean up again and restore originals if they existed + for mod_name in modules_to_clear: + if mod_name in sys.modules: + del sys.modules[mod_name] # Remove if test imported it + # Restore original modules for mod_name, original_mod in original_modules.items(): if original_mod: sys.modules[mod_name] = original_mod - elif mod_name in sys.modules: - # If it wasn't there before but is now, remove it - del sys.modules[mod_name] - - -# --- Test Case 1: JSON types available --- - -def test_all_includes_json_when_available(cleanup_imports_for_all): - """ - Test that __all__ includes JSON types when JSONArray and JSONDtype are available. - """ - # No patching needed for the 'else' block, assume normal import works - # and JSONArray/JSONDtype are truthy. - import db_dtypes - - expected_all = [ - "__version__", - "DateArray", - "DateDtype", - "JSONDtype", - "JSONArray", - "JSONArrowType", - "TimeArray", - "TimeDtype", - ] - # Use set comparison for order independence, as __all__ order isn't critical - assert set(db_dtypes.__all__) == set(expected_all) - # Explicitly check presence of JSON types - assert "JSONDtype" in db_dtypes.__all__ - assert "JSONArray" in db_dtypes.__all__ - assert "JSONArrowType" in db_dtypes.__all__ - - -# --- Test Case 2: JSON types unavailable --- @pytest.mark.parametrize( - "patch_target_name", + "mock_version_tuple, version_str, expect_warning", [ - "JSONArray", - "JSONDtype", - # Add both if needed, though one is sufficient to trigger the 'if' - # ("JSONArray", "JSONDtype"), + # Cases expected to warn + ((3, 7, 10), "3.7.10", True), + ((3, 7, 0), "3.7.0", True), + ((3, 8, 5), "3.8.5", True), + ((3, 8, 12), "3.8.12", True), + # Cases NOT expected to warn + ((3, 9, 1), "3.9.1", False), + ((3, 10, 0), "3.10.0", False), + ((3, 11, 2), "3.11.2", False), + ((3, 12, 0), "3.12.0", False), ] ) -def test_all_excludes_json_when_unavailable(cleanup_imports_for_all, patch_target_name): - """ - Test that __all__ excludes JSON types when JSONArray or JSONDtype is unavailable (falsy). +def test_python_version_warning_on_import(mock_version_tuple, version_str, expect_warning): + """Test that a FutureWarning is raised ONLY for Python 3.7 or 3.8 during import. """ - patch_path = f"{MODULE_PATH}.{patch_target_name}" - - # Patch one of the JSON types to be None *before* importing db_dtypes. - # This simulates the condition `if not JSONArray or not JSONDtype:` being true. - with mock.patch(patch_path, None): - # Need to ensure the json submodule itself is loaded if patching its contents - # If the patch target is directly in __init__, this isn't needed. - # Assuming JSONArray/JSONDtype are imported *into* __init__ from .json: - try: - import db_dtypes.json - except ImportError: - # Handle cases where the json module might genuinely be missing - pass - - # Now import the main module, which will evaluate __all__ - import db_dtypes - - expected_all = [ - "__version__", - "DateArray", - "DateDtype", - "TimeArray", - "TimeDtype", - ] - # Use set comparison for order independence - assert set(db_dtypes.__all__) == set(expected_all) - # Explicitly check absence of JSON types - assert "JSONDtype" not in db_dtypes.__all__ - assert "JSONArray" not in db_dtypes.__all__ - assert "JSONArrowType" not in db_dtypes.__all__ \ No newline at end of file + + # Create a mock function that returns the desired version tuple + mock_extract_func = mock.Mock(return_value=mock_version_tuple) + + # Create a mock module object for _versions_helpers + mock_helpers_module = types.ModuleType(HELPER_MODULE_PATH) + mock_helpers_module.extract_runtime_version = mock_extract_func + + # Use mock.patch.dict to temporarily replace the module in sys.modules + # This ensures that when db_dtypes.__init__ does `from . import _versions_helpers`, + # it gets our mock module. + with mock.patch.dict(sys.modules, {HELPER_MODULE_PATH: mock_helpers_module}): + if expect_warning: + with pytest.warns(FutureWarning) as record: + # The import will now use the mocked _versions_helpers module + import db_dtypes + + assert len(record) == 1 + warning_message = str(record[0].message) + assert "longer supports Python 3.7 and Python 3.8" in warning_message + assert f"Your Python version is {version_str}" in warning_message + assert "https://cloud.google.com/python/docs/supported-python-versions" in warning_message + else: + with warnings.catch_warnings(record=True) as record: + warnings.simplefilter("always") + # The import will now use the mocked _versions_helpers module + import db_dtypes + + found_warning = False + for w in record: + if (issubclass(w.category, FutureWarning) and + "longer supports Python 3.7 and Python 3.8" in str(w.message)): + found_warning = True + break + assert not found_warning, ( + f"Unexpected FutureWarning raised for Python version {version_str}" + ) From 6a5d943866b2d680ab1a0456a22dea5623960428 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Wed, 23 Apr 2025 15:05:30 +0000 Subject: [PATCH 13/17] Updates json array type tests to account for unittest coverage --- db_dtypes/json.py | 10 ++++-- tests/unit/test__init__.py | 69 +++++++++++++++++++++++++++++++++++--- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/db_dtypes/json.py b/db_dtypes/json.py index 37aad83..c6443ec 100644 --- a/db_dtypes/json.py +++ b/db_dtypes/json.py @@ -275,7 +275,11 @@ def __hash__(self) -> int: def to_pandas_dtype(self): return JSONDtype() - # Register the type to be included in RecordBatches, sent over IPC and received in -# another Python process. -pa.register_extension_type(JSONArrowType()) +# another Python process. Also handle potential pre-registration +try: + pa.register_extension_type(JSONArrowType()) +except pa.ArrowKeyError: + # Type 'dbjson' might already be registered if the module is reloaded, + # which is okay. + pass \ No newline at end of file diff --git a/tests/unit/test__init__.py b/tests/unit/test__init__.py index 8fb8a85..f9edaa8 100644 --- a/tests/unit/test__init__.py +++ b/tests/unit/test__init__.py @@ -3,12 +3,13 @@ import types import warnings from unittest import mock +import pyarrow as pa # The module where the version check code resides MODULE_PATH = "db_dtypes" HELPER_MODULE_PATH = f"{MODULE_PATH}._versions_helpers" -@pytest.fixture(autouse=True) +@pytest.fixture def cleanup_imports(): """Ensures the target module and its helper are removed from sys.modules before each test, allowing for clean imports with patching. @@ -48,7 +49,7 @@ def cleanup_imports(): ((3, 12, 0), "3.12.0", False), ] ) -def test_python_version_warning_on_import(mock_version_tuple, version_str, expect_warning): +def test_python_version_warning_on_import(mock_version_tuple, version_str, expect_warning, cleanup_imports): """Test that a FutureWarning is raised ONLY for Python 3.7 or 3.8 during import. """ @@ -71,8 +72,6 @@ def test_python_version_warning_on_import(mock_version_tuple, version_str, expec assert len(record) == 1 warning_message = str(record[0].message) assert "longer supports Python 3.7 and Python 3.8" in warning_message - assert f"Your Python version is {version_str}" in warning_message - assert "https://cloud.google.com/python/docs/supported-python-versions" in warning_message else: with warnings.catch_warnings(record=True) as record: warnings.simplefilter("always") @@ -88,3 +87,65 @@ def test_python_version_warning_on_import(mock_version_tuple, version_str, expec assert not found_warning, ( f"Unexpected FutureWarning raised for Python version {version_str}" ) + +# --- Test Case 1: JSON types available --- + +@pytest.fixture +def cleanup_imports_for_all(request): + """ + Ensures the target module and its dependencies potentially affecting + __all__ are removed from sys.modules before and after each test, + allowing for clean imports with patching. Also handles PyArrow extension type registration. + """ + + # Modules that might be checked or imported in __init__ + modules_to_clear = [ + MODULE_PATH, + f"{MODULE_PATH}.core", + f"{MODULE_PATH}.json", + f"{MODULE_PATH}.version", + f"{MODULE_PATH}._versions_helpers", + ] + original_modules = {} + + # Store original modules and remove them + for mod_name in modules_to_clear: + original_modules[mod_name] = sys.modules.get(mod_name) + if mod_name in sys.modules: + del sys.modules[mod_name] + + yield # Run the test + + # Restore original modules after test + for mod_name, original_mod in original_modules.items(): + if original_mod: + sys.modules[mod_name] = original_mod + elif mod_name in sys.modules: + # If it wasn't there before but is now, remove it + del sys.modules[mod_name] + +def test_all_includes_json_when_available(cleanup_imports_for_all): + """ + Test that __all__ includes JSON types when JSONArray and JSONDtype are available. + """ + + # No patching needed for the 'else' block, assume normal import works + # and JSONArray/JSONDtype are truthy. + import db_dtypes + + expected_all = [ + "__version__", + "DateArray", + "DateDtype", + "JSONDtype", + "JSONArray", + "JSONArrowType", + "TimeArray", + "TimeDtype", + ] + # Use set comparison for order independence, as __all__ order isn't critical + assert set(db_dtypes.__all__) == set(expected_all) + # Explicitly check presence of JSON types + assert "JSONDtype" in db_dtypes.__all__ + assert "JSONArray" in db_dtypes.__all__ + assert "JSONArrowType" in db_dtypes.__all__ From 1bbd5f7c096dd776a178cf99c071008c78ae3053 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Wed, 23 Apr 2025 21:43:22 +0000 Subject: [PATCH 14/17] updates python version checks to ensure coverage --- db_dtypes/__init__.py | 69 +++++++----- tests/unit/test__init__.py | 212 +++++++++++++++++-------------------- 2 files changed, 144 insertions(+), 137 deletions(-) diff --git a/db_dtypes/__init__.py b/db_dtypes/__init__.py index 5cd49da..06fc423 100644 --- a/db_dtypes/__init__.py +++ b/db_dtypes/__init__.py @@ -337,34 +337,55 @@ def __sub__(self, other): return super().__sub__(other) +# Inside db_dtypes/__init__.py (TEMPORARY DEBUG) +print(f"DEBUG: Inside __init__, JSONArray type: {type(JSONArray)}, value: {repr(JSONArray)}") +print(f"DEBUG: Inside __init__, JSONDtype type: {type(JSONDtype)}, value: {repr(JSONDtype)}") -sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version() -if sys_major == 3 and sys_minor in (7, 8): - warnings.warn( - "The python-bigquery library as well as the python-db-dtypes-pandas library no " - "longer supports Python 3.7 and Python 3.8. " - f"Your Python version is {sys_major}.{sys_minor}.{sys_micro}. We " - "recommend that you update soon to ensure ongoing support. For " - "more details, see: [Google Cloud Client Libraries Supported Python Versions policy](https://cloud.google.com/python/docs/supported-python-versions)", - FutureWarning, - ) - -if not JSONArray or not JSONDtype: - __all__ = [ +def _determine_all(json_array_type, json_dtype_type): + """Determines the list for __all__ based on JSON type availability.""" + base_all = [ "__version__", "DateArray", "DateDtype", "TimeArray", "TimeDtype", ] -else: - __all__ = [ - "__version__", - "DateArray", - "DateDtype", - "JSONDtype", - "JSONArray", - "JSONArrowType", - "TimeArray", - "TimeDtype", - ] + # Check if both JSON types are available (truthy) + if json_array_type and json_dtype_type: + # print("DEBUG: Condition FALSE, including JSON in __all__") # Keep if needed + return base_all + ["JSONDtype", "JSONArray", "JSONArrowType"] + else: + # print("DEBUG: Condition TRUE, excluding JSON from __all__") # Keep if needed + return base_all + +def _check_python_version(): + """Checks the runtime Python version and issues a warning if needed.""" + sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version() + if sys_major == 3 and sys_minor in (7, 8): + warnings.warn( + "The python-bigquery library as well as the python-db-dtypes-pandas library no " + "longer supports Python 3.7 and Python 3.8. " + f"Your Python version is {sys_major}.{sys_minor}.{sys_micro}. We " + "recommend that you update soon to ensure ongoing support. For " + "more details, see: [Google Cloud Client Libraries Supported Python Versions policy](https://cloud.google.com/python/docs/supported-python-versions)", + FutureWarning, + stacklevel=2, # Point warning to the caller of __init__ + ) + + +# sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version() +# if sys_major == 3 and sys_minor in (7, 8): +# warnings.warn( +# "The python-bigquery library as well as the python-db-dtypes-pandas library no " +# "longer supports Python 3.7 and Python 3.8. " +# f"Your Python version is {sys_major}.{sys_minor}.{sys_micro}. We " +# "recommend that you update soon to ensure ongoing support. For " +# "more details, see: [Google Cloud Client Libraries Supported Python Versions policy](https://cloud.google.com/python/docs/supported-python-versions)", +# FutureWarning, +# ) + +# Perform the version check +_check_python_version() + +# Assign __all__ by calling the function +__all__ = _determine_all(JSONArray, JSONDtype) diff --git a/tests/unit/test__init__.py b/tests/unit/test__init__.py index f9edaa8..8b66ba7 100644 --- a/tests/unit/test__init__.py +++ b/tests/unit/test__init__.py @@ -5,147 +5,133 @@ from unittest import mock import pyarrow as pa -# The module where the version check code resides +# Module paths used for mocking MODULE_PATH = "db_dtypes" HELPER_MODULE_PATH = f"{MODULE_PATH}._versions_helpers" +MOCK_EXTRACT_VERSION = f"{HELPER_MODULE_PATH}.extract_runtime_version" +MOCK_WARN = "warnings.warn" # Target the standard warnings module -@pytest.fixture -def cleanup_imports(): - """Ensures the target module and its helper are removed from sys.modules - before each test, allowing for clean imports with patching. +@pytest.mark.parametrize( + "mock_version_tuple, version_str", + [ + ((3, 7, 10), "3.7.10"), + ((3, 7, 0), "3.7.0"), + ((3, 8, 5), "3.8.5"), + ((3, 8, 12), "3.8.12"), + ] +) +def test_check_python_version_warns_on_unsupported(mock_version_tuple, version_str): """ + Test that _check_python_version issues a FutureWarning for Python 3.7/3.8. + """ + # Import the function under test directly + from db_dtypes import _check_python_version + + # Mock the helper function it calls and the warnings.warn function + with mock.patch(MOCK_EXTRACT_VERSION, return_value=mock_version_tuple), \ + mock.patch(MOCK_WARN) as mock_warn_call: + + _check_python_version() # Call the function + + # Assert that warnings.warn was called exactly once + mock_warn_call.assert_called_once() + + # Check the arguments passed to warnings.warn + args, kwargs = mock_warn_call.call_args + assert len(args) >= 1 # Should have at least the message + warning_message = args[0] + warning_category = args[1] if len(args) > 1 else kwargs.get('category') + + # Verify message content and category + assert "longer supports Python 3.7 and Python 3.8" in warning_message + assert f"Your Python version is {version_str}" in warning_message + assert "https://cloud.google.com/python/docs/supported-python-versions" in warning_message + assert warning_category == FutureWarning + # Optionally check stacklevel if important + assert kwargs.get('stacklevel') == 2 - # Store original modules that might exist - original_modules = {} - modules_to_clear = [MODULE_PATH, HELPER_MODULE_PATH] - for mod_name in modules_to_clear: - if mod_name in sys.modules: - original_modules[mod_name] = sys.modules[mod_name] - del sys.modules[mod_name] - - yield # Run the test - - # Clean up again and restore originals if they existed - for mod_name in modules_to_clear: - if mod_name in sys.modules: - del sys.modules[mod_name] # Remove if test imported it - # Restore original modules - for mod_name, original_mod in original_modules.items(): - if original_mod: - sys.modules[mod_name] = original_mod @pytest.mark.parametrize( - "mock_version_tuple, version_str, expect_warning", + "mock_version_tuple", [ - # Cases expected to warn - ((3, 7, 10), "3.7.10", True), - ((3, 7, 0), "3.7.0", True), - ((3, 8, 5), "3.8.5", True), - ((3, 8, 12), "3.8.12", True), - # Cases NOT expected to warn - ((3, 9, 1), "3.9.1", False), - ((3, 10, 0), "3.10.0", False), - ((3, 11, 2), "3.11.2", False), - ((3, 12, 0), "3.12.0", False), + (3, 9, 1), + (3, 10, 0), + (3, 11, 2), + (3, 12, 0), + (4, 0, 0), # Future version + (3, 6, 0), # Older unsupported, but not 3.7/3.8 ] ) -def test_python_version_warning_on_import(mock_version_tuple, version_str, expect_warning, cleanup_imports): - """Test that a FutureWarning is raised ONLY for Python 3.7 or 3.8 during import. +def test_check_python_version_does_not_warn_on_supported(mock_version_tuple): """ - - # Create a mock function that returns the desired version tuple - mock_extract_func = mock.Mock(return_value=mock_version_tuple) - - # Create a mock module object for _versions_helpers - mock_helpers_module = types.ModuleType(HELPER_MODULE_PATH) - mock_helpers_module.extract_runtime_version = mock_extract_func - - # Use mock.patch.dict to temporarily replace the module in sys.modules - # This ensures that when db_dtypes.__init__ does `from . import _versions_helpers`, - # it gets our mock module. - with mock.patch.dict(sys.modules, {HELPER_MODULE_PATH: mock_helpers_module}): - if expect_warning: - with pytest.warns(FutureWarning) as record: - # The import will now use the mocked _versions_helpers module - import db_dtypes - - assert len(record) == 1 - warning_message = str(record[0].message) - assert "longer supports Python 3.7 and Python 3.8" in warning_message - else: - with warnings.catch_warnings(record=True) as record: - warnings.simplefilter("always") - # The import will now use the mocked _versions_helpers module - import db_dtypes - - found_warning = False - for w in record: - if (issubclass(w.category, FutureWarning) and - "longer supports Python 3.7 and Python 3.8" in str(w.message)): - found_warning = True - break - assert not found_warning, ( - f"Unexpected FutureWarning raised for Python version {version_str}" - ) - -# --- Test Case 1: JSON types available --- - -@pytest.fixture -def cleanup_imports_for_all(request): - """ - Ensures the target module and its dependencies potentially affecting - __all__ are removed from sys.modules before and after each test, - allowing for clean imports with patching. Also handles PyArrow extension type registration. + Test that _check_python_version does NOT issue a warning for other versions. """ + # Import the function under test directly + from db_dtypes import _check_python_version - # Modules that might be checked or imported in __init__ - modules_to_clear = [ - MODULE_PATH, - f"{MODULE_PATH}.core", - f"{MODULE_PATH}.json", - f"{MODULE_PATH}.version", - f"{MODULE_PATH}._versions_helpers", - ] - original_modules = {} + # Mock the helper function it calls and the warnings.warn function + with mock.patch(MOCK_EXTRACT_VERSION, return_value=mock_version_tuple), \ + mock.patch(MOCK_WARN) as mock_warn_call: - # Store original modules and remove them - for mod_name in modules_to_clear: - original_modules[mod_name] = sys.modules.get(mod_name) - if mod_name in sys.modules: - del sys.modules[mod_name] + _check_python_version() # Call the function - yield # Run the test + # Assert that warnings.warn was NOT called + mock_warn_call.assert_not_called() - # Restore original modules after test - for mod_name, original_mod in original_modules.items(): - if original_mod: - sys.modules[mod_name] = original_mod - elif mod_name in sys.modules: - # If it wasn't there before but is now, remove it - del sys.modules[mod_name] -def test_all_includes_json_when_available(cleanup_imports_for_all): +def test_determine_all_includes_json_when_available(): """ - Test that __all__ includes JSON types when JSONArray and JSONDtype are available. + Test that _determine_all includes JSON types when both are truthy. """ + # Import the function directly for testing + from db_dtypes import _determine_all - # No patching needed for the 'else' block, assume normal import works - # and JSONArray/JSONDtype are truthy. - import db_dtypes + # Simulate available types (can be any truthy object) + mock_json_array = object() + mock_json_dtype = object() + + result = _determine_all(mock_json_array, mock_json_dtype) expected_all = [ "__version__", "DateArray", "DateDtype", + "TimeArray", + "TimeDtype", "JSONDtype", "JSONArray", "JSONArrowType", + ] + assert set(result) == set(expected_all) + assert "JSONDtype" in result + assert "JSONArray" in result + assert "JSONArrowType" in result + +@pytest.mark.parametrize( + "mock_array, mock_dtype", + [ + (None, object()), # JSONArray is None + (object(), None), # JSONDtype is None + (None, None), # Both are None + ] +) +def test_determine_all_excludes_json_when_unavailable(mock_array, mock_dtype): + """ + Test that _determine_all excludes JSON types if either is falsy. + """ + # Import the function directly for testing + from db_dtypes import _determine_all + + result = _determine_all(mock_array, mock_dtype) + + expected_all = [ + "__version__", + "DateArray", + "DateDtype", "TimeArray", "TimeDtype", ] - # Use set comparison for order independence, as __all__ order isn't critical - assert set(db_dtypes.__all__) == set(expected_all) - # Explicitly check presence of JSON types - assert "JSONDtype" in db_dtypes.__all__ - assert "JSONArray" in db_dtypes.__all__ - assert "JSONArrowType" in db_dtypes.__all__ + assert set(result) == set(expected_all) + assert "JSONDtype" not in result + assert "JSONArray" not in result + assert "JSONArrowType" not in result \ No newline at end of file From 96a5e97ba2a04968840689e03c370972e49f49cb Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Wed, 23 Apr 2025 22:19:38 +0000 Subject: [PATCH 15/17] update json test for unittest coverage --- tests/unit/test_json.py | 66 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/unit/test_json.py b/tests/unit/test_json.py index d15cfc7..a3d00ef 100644 --- a/tests/unit/test_json.py +++ b/tests/unit/test_json.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import importlib import json +import sys import numpy as np import pandas as pd @@ -224,3 +226,67 @@ def test_json_arrow_record_batch(): == '{"null_field":null,"order":{"address":{"city":"Anytown","street":"123 Main St"},"items":["book","pen","computer"],"total":15}}' ) assert s[6] == "null" + +@pytest.fixture +def cleanup_json_module_for_reload(): + """ + Fixture to ensure db_dtypes.json is registered and then removed + from sys.modules to allow testing the registration except block via reload. + """ + json_module_name = "db_dtypes.json" + original_module = sys.modules.get(json_module_name) + + # 1. Ensure the type is registered initially (usually by the first import) + try: + # Make sure the module is loaded so the type exists + import db_dtypes.json + # Explicitly register just in case it wasn't, or was cleaned up elsewhere. + # This might raise ArrowKeyError itself if already registered, which is fine here. + pa.register_extension_type(db_dtypes.json.JSONArrowType()) + except pa.ArrowKeyError: + pass # Already registered is the state we want before the test runs + except ImportError: + pytest.skip("Could not import db_dtypes.json to set up test.") + + # 2. Remove the module from sys.modules so importlib.reload re-executes it + if json_module_name in sys.modules: + del sys.modules[json_module_name] + + yield # Run the test that uses this fixture + + # 3. Cleanup: Put the original module back if it existed + # This helps isolate from other tests that might import db_dtypes.json + if original_module: + sys.modules[json_module_name] = original_module + elif json_module_name in sys.modules: + # If the test re-imported it but it wasn't there originally, remove it + del sys.modules[json_module_name] + + # Note: PyArrow doesn't have a public API to unregister types easily. + # Relying on module isolation/reloading is a common testing pattern. + + +def test_json_arrow_type_reregistration_is_handled(cleanup_json_module_for_reload): + """ + Verify that attempting to re-register JSONArrowType via module reload + is caught by the except block and does not raise an error. + """ + try: + # Re-importing the module after the fixture removed it from sys.modules + # forces Python to execute the module's top-level code again. + # This includes the pa.register_extension_type call. + import db_dtypes.json + + # If the import completes without raising pa.ArrowKeyError, + # it means the 'except ArrowKeyError: pass' block worked as expected. + assert True, "Module re-import completed without error, except block likely worked." + + except pa.ArrowKeyError: + # If this exception escapes, the except block in db_dtypes/json.py failed. + pytest.fail( + "pa.ArrowKeyError was raised during module reload, " + "indicating the except block failed." + ) + except Exception as e: + # Catch any other unexpected error during the reload for better debugging. + pytest.fail(f"An unexpected exception occurred during module reload: {e}") \ No newline at end of file From 8af9beec0b7429b5e4c6754e45a2c09cc25b7c24 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 24 Apr 2025 10:55:54 +0000 Subject: [PATCH 16/17] Update pandas_backports unittests to ensure coverage --- tests/unit/test_pandas_backports.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unit/test_pandas_backports.py b/tests/unit/test_pandas_backports.py index eb68b6a..f0c5a02 100644 --- a/tests/unit/test_pandas_backports.py +++ b/tests/unit/test_pandas_backports.py @@ -35,3 +35,20 @@ def test_import_default_module_not_found(mock_import): default_class = type("OpsMixin", (), {}) # Dummy class result = pandas_backports.import_default("module_name", default=default_class) assert result == default_class + + +@mock.patch("builtins.__import__") +def test_import_default_force_true(mock_import): + """ + Test that when force=True, the default is returned immediately + without attempting an import. + """ + default_class = type("ForcedMixin", (), {}) # A dummy class + + result = pandas_backports.import_default( + "any_module_name", force=True, default=default_class + ) + + # Assert that the returned value is the default class itself + assert result is default_class + From 78574c0db78cc2f90223363a23a4ff0874d823b1 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 24 Apr 2025 15:31:23 +0000 Subject: [PATCH 17/17] Updates per review comments --- db_dtypes/__init__.py | 25 +------ db_dtypes/json.py | 3 +- .../date/test_date_compliance_1_5.py | 3 - .../time/test_time_compliance_1_5.py | 3 - tests/unit/test__init__.py | 75 +++++++++++-------- tests/unit/test_json.py | 21 +++--- tests/unit/test_pandas_backports.py | 1 - 7 files changed, 57 insertions(+), 74 deletions(-) diff --git a/db_dtypes/__init__.py b/db_dtypes/__init__.py index 06fc423..0373027 100644 --- a/db_dtypes/__init__.py +++ b/db_dtypes/__init__.py @@ -28,8 +28,7 @@ import pyarrow.compute from db_dtypes import core -from db_dtypes.json import JSONArray, JSONArrowType, JSONDtype -from db_dtypes.version import __version__ +from db_dtypes.json import JSONArray, JSONDtype from . import _versions_helpers @@ -337,9 +336,6 @@ def __sub__(self, other): return super().__sub__(other) -# Inside db_dtypes/__init__.py (TEMPORARY DEBUG) -print(f"DEBUG: Inside __init__, JSONArray type: {type(JSONArray)}, value: {repr(JSONArray)}") -print(f"DEBUG: Inside __init__, JSONDtype type: {type(JSONDtype)}, value: {repr(JSONDtype)}") def _determine_all(json_array_type, json_dtype_type): """Determines the list for __all__ based on JSON type availability.""" @@ -352,12 +348,11 @@ def _determine_all(json_array_type, json_dtype_type): ] # Check if both JSON types are available (truthy) if json_array_type and json_dtype_type: - # print("DEBUG: Condition FALSE, including JSON in __all__") # Keep if needed return base_all + ["JSONDtype", "JSONArray", "JSONArrowType"] else: - # print("DEBUG: Condition TRUE, excluding JSON from __all__") # Keep if needed return base_all + def _check_python_version(): """Checks the runtime Python version and issues a warning if needed.""" sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version() @@ -369,23 +364,9 @@ def _check_python_version(): "recommend that you update soon to ensure ongoing support. For " "more details, see: [Google Cloud Client Libraries Supported Python Versions policy](https://cloud.google.com/python/docs/supported-python-versions)", FutureWarning, - stacklevel=2, # Point warning to the caller of __init__ + stacklevel=2, # Point warning to the caller of __init__ ) - -# sys_major, sys_minor, sys_micro = _versions_helpers.extract_runtime_version() -# if sys_major == 3 and sys_minor in (7, 8): -# warnings.warn( -# "The python-bigquery library as well as the python-db-dtypes-pandas library no " -# "longer supports Python 3.7 and Python 3.8. " -# f"Your Python version is {sys_major}.{sys_minor}.{sys_micro}. We " -# "recommend that you update soon to ensure ongoing support. For " -# "more details, see: [Google Cloud Client Libraries Supported Python Versions policy](https://cloud.google.com/python/docs/supported-python-versions)", -# FutureWarning, -# ) - -# Perform the version check _check_python_version() -# Assign __all__ by calling the function __all__ = _determine_all(JSONArray, JSONDtype) diff --git a/db_dtypes/json.py b/db_dtypes/json.py index c6443ec..6159316 100644 --- a/db_dtypes/json.py +++ b/db_dtypes/json.py @@ -275,6 +275,7 @@ def __hash__(self) -> int: def to_pandas_dtype(self): return JSONDtype() + # Register the type to be included in RecordBatches, sent over IPC and received in # another Python process. Also handle potential pre-registration try: @@ -282,4 +283,4 @@ def to_pandas_dtype(self): except pa.ArrowKeyError: # Type 'dbjson' might already be registered if the module is reloaded, # which is okay. - pass \ No newline at end of file + pass diff --git a/tests/compliance/date/test_date_compliance_1_5.py b/tests/compliance/date/test_date_compliance_1_5.py index 6723443..7993d25 100644 --- a/tests/compliance/date/test_date_compliance_1_5.py +++ b/tests/compliance/date/test_date_compliance_1_5.py @@ -24,8 +24,5 @@ import pytest # NDArrayBacked2DTests suite added in https://github.com/pandas-dev/pandas/pull/44974 -pytest.importorskip("pandas") - - class Test2DCompat(base.NDArrayBacked2DTests): pass diff --git a/tests/compliance/time/test_time_compliance_1_5.py b/tests/compliance/time/test_time_compliance_1_5.py index 6723443..7993d25 100644 --- a/tests/compliance/time/test_time_compliance_1_5.py +++ b/tests/compliance/time/test_time_compliance_1_5.py @@ -24,8 +24,5 @@ import pytest # NDArrayBacked2DTests suite added in https://github.com/pandas-dev/pandas/pull/44974 -pytest.importorskip("pandas") - - class Test2DCompat(base.NDArrayBacked2DTests): pass diff --git a/tests/unit/test__init__.py b/tests/unit/test__init__.py index 8b66ba7..7ae260c 100644 --- a/tests/unit/test__init__.py +++ b/tests/unit/test__init__.py @@ -1,15 +1,27 @@ -import sys -import pytest -import types -import warnings +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from unittest import mock -import pyarrow as pa + +import pytest # Module paths used for mocking MODULE_PATH = "db_dtypes" HELPER_MODULE_PATH = f"{MODULE_PATH}._versions_helpers" MOCK_EXTRACT_VERSION = f"{HELPER_MODULE_PATH}.extract_runtime_version" -MOCK_WARN = "warnings.warn" # Target the standard warnings module +MOCK_WARN = "warnings.warn" # Target the standard warnings module + @pytest.mark.parametrize( "mock_version_tuple, version_str", @@ -18,37 +30,33 @@ ((3, 7, 0), "3.7.0"), ((3, 8, 5), "3.8.5"), ((3, 8, 12), "3.8.12"), - ] + ], ) def test_check_python_version_warns_on_unsupported(mock_version_tuple, version_str): """ Test that _check_python_version issues a FutureWarning for Python 3.7/3.8. """ - # Import the function under test directly + from db_dtypes import _check_python_version # Mock the helper function it calls and the warnings.warn function - with mock.patch(MOCK_EXTRACT_VERSION, return_value=mock_version_tuple), \ - mock.patch(MOCK_WARN) as mock_warn_call: - - _check_python_version() # Call the function + with mock.patch(MOCK_EXTRACT_VERSION, return_value=mock_version_tuple), mock.patch( + MOCK_WARN + ) as mock_warn_call: + _check_python_version() # Call the function # Assert that warnings.warn was called exactly once mock_warn_call.assert_called_once() # Check the arguments passed to warnings.warn args, kwargs = mock_warn_call.call_args - assert len(args) >= 1 # Should have at least the message + assert len(args) >= 1 # Should have at least the message warning_message = args[0] - warning_category = args[1] if len(args) > 1 else kwargs.get('category') + warning_category = args[1] if len(args) > 1 else kwargs.get("category") # Verify message content and category assert "longer supports Python 3.7 and Python 3.8" in warning_message - assert f"Your Python version is {version_str}" in warning_message - assert "https://cloud.google.com/python/docs/supported-python-versions" in warning_message assert warning_category == FutureWarning - # Optionally check stacklevel if important - assert kwargs.get('stacklevel') == 2 @pytest.mark.parametrize( @@ -58,22 +66,22 @@ def test_check_python_version_warns_on_unsupported(mock_version_tuple, version_s (3, 10, 0), (3, 11, 2), (3, 12, 0), - (4, 0, 0), # Future version - (3, 6, 0), # Older unsupported, but not 3.7/3.8 - ] + (4, 0, 0), # Future version + (3, 6, 0), # Older unsupported, but not 3.7/3.8 + ], ) def test_check_python_version_does_not_warn_on_supported(mock_version_tuple): """ Test that _check_python_version does NOT issue a warning for other versions. """ - # Import the function under test directly + from db_dtypes import _check_python_version # Mock the helper function it calls and the warnings.warn function - with mock.patch(MOCK_EXTRACT_VERSION, return_value=mock_version_tuple), \ - mock.patch(MOCK_WARN) as mock_warn_call: - - _check_python_version() # Call the function + with mock.patch(MOCK_EXTRACT_VERSION, return_value=mock_version_tuple), mock.patch( + MOCK_WARN + ) as mock_warn_call: + _check_python_version() # Assert that warnings.warn was NOT called mock_warn_call.assert_not_called() @@ -83,7 +91,7 @@ def test_determine_all_includes_json_when_available(): """ Test that _determine_all includes JSON types when both are truthy. """ - # Import the function directly for testing + from db_dtypes import _determine_all # Simulate available types (can be any truthy object) @@ -107,19 +115,20 @@ def test_determine_all_includes_json_when_available(): assert "JSONArray" in result assert "JSONArrowType" in result + @pytest.mark.parametrize( "mock_array, mock_dtype", [ - (None, object()), # JSONArray is None - (object(), None), # JSONDtype is None - (None, None), # Both are None - ] + (None, object()), # JSONArray is None + (object(), None), # JSONDtype is None + (None, None), # Both are None + ], ) def test_determine_all_excludes_json_when_unavailable(mock_array, mock_dtype): """ Test that _determine_all excludes JSON types if either is falsy. """ - # Import the function directly for testing + from db_dtypes import _determine_all result = _determine_all(mock_array, mock_dtype) @@ -134,4 +143,4 @@ def test_determine_all_excludes_json_when_unavailable(mock_array, mock_dtype): assert set(result) == set(expected_all) assert "JSONDtype" not in result assert "JSONArray" not in result - assert "JSONArrowType" not in result \ No newline at end of file + assert "JSONArrowType" not in result diff --git a/tests/unit/test_json.py b/tests/unit/test_json.py index a3d00ef..93a5fef 100644 --- a/tests/unit/test_json.py +++ b/tests/unit/test_json.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import importlib import json import sys @@ -227,19 +226,22 @@ def test_json_arrow_record_batch(): ) assert s[6] == "null" + @pytest.fixture def cleanup_json_module_for_reload(): """ Fixture to ensure db_dtypes.json is registered and then removed from sys.modules to allow testing the registration except block via reload. """ + json_module_name = "db_dtypes.json" original_module = sys.modules.get(json_module_name) - # 1. Ensure the type is registered initially (usually by the first import) + # Ensure the type is registered initially (usually by the first import) try: # Make sure the module is loaded so the type exists import db_dtypes.json + # Explicitly register just in case it wasn't, or was cleaned up elsewhere. # This might raise ArrowKeyError itself if already registered, which is fine here. pa.register_extension_type(db_dtypes.json.JSONArrowType()) @@ -248,13 +250,13 @@ def cleanup_json_module_for_reload(): except ImportError: pytest.skip("Could not import db_dtypes.json to set up test.") - # 2. Remove the module from sys.modules so importlib.reload re-executes it + # Remove the module from sys.modules so importlib.reload re-executes it if json_module_name in sys.modules: del sys.modules[json_module_name] yield # Run the test that uses this fixture - # 3. Cleanup: Put the original module back if it existed + # Cleanup: Put the original module back if it existed # This helps isolate from other tests that might import db_dtypes.json if original_module: sys.modules[json_module_name] = original_module @@ -275,11 +277,9 @@ def test_json_arrow_type_reregistration_is_handled(cleanup_json_module_for_reloa # Re-importing the module after the fixture removed it from sys.modules # forces Python to execute the module's top-level code again. # This includes the pa.register_extension_type call. - import db_dtypes.json - - # If the import completes without raising pa.ArrowKeyError, - # it means the 'except ArrowKeyError: pass' block worked as expected. - assert True, "Module re-import completed without error, except block likely worked." + assert ( + True + ), "Module re-import completed without error, except block likely worked." except pa.ArrowKeyError: # If this exception escapes, the except block in db_dtypes/json.py failed. @@ -288,5 +288,4 @@ def test_json_arrow_type_reregistration_is_handled(cleanup_json_module_for_reloa "indicating the except block failed." ) except Exception as e: - # Catch any other unexpected error during the reload for better debugging. - pytest.fail(f"An unexpected exception occurred during module reload: {e}") \ No newline at end of file + pytest.fail(f"An unexpected exception occurred during module reload: {e}") diff --git a/tests/unit/test_pandas_backports.py b/tests/unit/test_pandas_backports.py index f0c5a02..cb78304 100644 --- a/tests/unit/test_pandas_backports.py +++ b/tests/unit/test_pandas_backports.py @@ -51,4 +51,3 @@ def test_import_default_force_true(mock_import): # Assert that the returned value is the default class itself assert result is default_class -