diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index bae6e96..55cd410 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -11,7 +11,6 @@ branchProtectionRules: - 'cla/google' - 'docs' - 'lint' - - 'unit (3.8)' - 'unit (3.9)' - 'unit (3.10)' - 'unit (3.11)' 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/CONTRIBUTING.rst b/CONTRIBUTING.rst index 0bda74a..c333038 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -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 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/db_dtypes/__init__.py b/db_dtypes/__init__.py index 6656671..c2b23da 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,7 +28,7 @@ import pyarrow.compute from db_dtypes import core -from db_dtypes.version import __version__ +from db_dtypes.json import JSONArray, JSONDtype, JSONArrowType # noqa: F401 from . import _versions_helpers @@ -47,15 +46,6 @@ _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 - - @pandas.api.extensions.register_extension_dtype class TimeDtype(core.BaseDatetimeDtype): """ @@ -364,23 +354,13 @@ def _check_python_version(): _check_python_version() - -if not JSONArray or not JSONDtype: - __all__ = [ - "__version__", - "DateArray", - "DateDtype", - "TimeArray", - "TimeDtype", - ] -else: - __all__ = [ - "__version__", - "DateArray", - "DateDtype", - "JSONDtype", - "JSONArray", - "JSONArrowType", - "TimeArray", - "TimeDtype", - ] +__all__ = [ + "__version__", + "DateArray", + "DateDtype", + "TimeArray", + "TimeDtype", + "JSONDtype", + "JSONArray", + "JSONArrowType", +] diff --git a/db_dtypes/core.py b/db_dtypes/core.py index 7c9eb6b..926a110 100644 --- a/db_dtypes/core.py +++ b/db_dtypes/core.py @@ -18,6 +18,7 @@ import pandas import pandas.api.extensions from pandas.api.types import is_dtype_equal, is_list_like, is_scalar, pandas_dtype +from pandas.core.arrays import _mixins from db_dtypes import pandas_backports @@ -42,9 +43,7 @@ def construct_from_string(cls, name: str): return cls() -class BaseDatetimeArray( - pandas_backports.OpsMixin, pandas_backports.NDArrayBackedExtensionArray -): +class BaseDatetimeArray(pandas_backports.OpsMixin, _mixins.NDArrayBackedExtensionArray): # scalar used to denote NA value inside our self._ndarray, e.g. -1 for # Categorical, iNaT for Period. Outside of object dtype, self.isna() should # be exactly locations in self._ndarray with _internal_fill_value. See: @@ -186,9 +185,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( (), {"out": out, "overwrite_input": overwrite_input, "keepdims": keepdims}, diff --git a/db_dtypes/pandas_backports.py b/db_dtypes/pandas_backports.py index f8009ea..378bb41 100644 --- a/db_dtypes/pandas_backports.py +++ b/db_dtypes/pandas_backports.py @@ -19,18 +19,13 @@ the versions in the later versions of pandas. """ -from typing import Any - -import numpy import packaging.version import pandas -from pandas.api.types import is_integer import pandas.compat.numpy.function -import pandas.core.nanops pandas_release = packaging.version.parse(pandas.__version__).release -# Create aliases for private methods in case they move in a future version. +# # Create aliases for private methods in case they move in a future version. nanall = pandas.core.nanops.nanall nanany = pandas.core.nanops.nanany nanmax = pandas.core.nanops.nanmax @@ -40,9 +35,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): @@ -78,83 +72,3 @@ def import_default(module_name, force=False, default=None): class OpsMixin: def _cmp_method(self, other, op): # pragma: NO COVER return NotImplemented - - -# 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)) -class NDArrayBackedExtensionArray(pandas.core.arrays.base.ExtensionArray): - def __init__(self, values, dtype): - assert isinstance(values, numpy.ndarray) - self._ndarray = values - self._dtype = dtype - - @classmethod - def _from_backing_data(cls, data): - return cls(data, data.dtype) - - def __getitem__(self, index): - value = self._ndarray[index] - if is_integer(index): - return self._box_func(value) - return self.__class__(value, self._dtype) - - def __setitem__(self, index, value): - self._ndarray[index] = self._validate_setitem_value(value) - - def __len__(self): - return len(self._ndarray) - - @property - def shape(self): - return self._ndarray.shape - - @property - def ndim(self) -> int: - return self._ndarray.ndim - - @property - def size(self) -> int: - return self._ndarray.size - - @property - def nbytes(self) -> int: - return self._ndarray.nbytes - - def copy(self): - return self[:] - - def repeat(self, n): - return self.__class__(self._ndarray.repeat(n), self._dtype) - - def take( - self, - indices, - *, - allow_fill: bool = False, - fill_value: Any = None, - axis: int = 0, - ): - from pandas.core.algorithms import take - - if allow_fill: - fill_value = self._validate_scalar(fill_value) - - new_data = take( - self._ndarray, - indices, - allow_fill=allow_fill, - fill_value=fill_value, - axis=axis, - ) - return self._from_backing_data(new_data) - - @classmethod - def _concat_same_type(cls, to_concat, axis=0): - dtypes = {str(x.dtype) for x in to_concat} - if len(dtypes) != 1: - raise ValueError("to_concat must have the same dtype (tz)", dtypes) - - new_values = [x._ndarray for x in to_concat] - new_values = numpy.concatenate(new_values, axis=axis) - return to_concat[0]._from_backing_data(new_values) # type: ignore[arg-type] diff --git a/db_dtypes/version.py b/db_dtypes/version.py index c97e3ca..d74293b 100644 --- a/db_dtypes/version.py +++ b/db_dtypes/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.4.2" +__version__ = "1.4.2" # pragma: NO COVER diff --git a/noxfile.py b/noxfile.py index b3c9450..a2444ac 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", @@ -88,7 +86,10 @@ nox.options.error_on_missing_interpreters = True -@nox.session(python=DEFAULT_PYTHON_VERSION) +# TODO: the linting process still uses python 3.8. +# As soon as that gets upgraded, we should be able to revert this session +# to using the DEFAULT_PYTHON_VERSION. +@nox.session(python="3.8") def lint(session): """Run linters. @@ -105,7 +106,11 @@ def lint(session): session.run("flake8", "db_dtypes", "tests") -@nox.session(python=DEFAULT_PYTHON_VERSION) +# TODO: the owlbot-python docker image still has python 3.8 installed ( +# and only 3.8). +# As soon as that gets upgraded, we should be able to revert this session +# to using the DEFAULT_PYTHON_VERSION. +@nox.session(python="3.8") def blacken(session): """Run black. Format code to uniform standard.""" session.install(BLACK_VERSION) @@ -137,7 +142,10 @@ def format(session): ) -@nox.session(python=DEFAULT_PYTHON_VERSION) +# TODO: the linting process still uses python 3.8. +# As soon as that gets upgraded, we should be able to revert this session +# to using the DEFAULT_PYTHON_VERSION. +@nox.session(python="3.8") def lint_setup_py(session): """Verify that setup.py is valid (including RST check).""" session.install("docutils", "pygments") 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/" 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 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/setup.py b/setup.py index 98bed9d..80a69b6 100644 --- a/setup.py +++ b/setup.py @@ -30,10 +30,10 @@ release_status = "Development Status :: 5 - Production/Stable" dependencies = [ - "packaging >= 17.0", - "pandas >= 1.2.0", - "pyarrow>=3.0.0", - "numpy >= 1.16.6", + "numpy >= 1.24.0", + "packaging >= 24.2.0", + "pandas >= 1.5.3", + "pyarrow >= 13.0.0", ] package_root = os.path.abspath(os.path.dirname(__file__)) @@ -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 diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt index afea9b0..1019c1c 100644 --- a/testing/constraints-3.9.txt +++ b/testing/constraints-3.9.txt @@ -1,3 +1,5 @@ # Make sure we test with pandas 1.5.3. The Python version isn't that relevant. +numpy==1.24.0 +packaging==24.2.0 pandas==1.5.3 -numpy==1.24.0 \ No newline at end of file +pyarrow==13.0.0 diff --git a/tests/compliance/date/test_date_compliance.py b/tests/compliance/date/test_date_compliance.py index 038005a..52b9c04 100644 --- a/tests/compliance/date/test_date_compliance.py +++ b/tests/compliance/date/test_date_compliance.py @@ -117,3 +117,9 @@ class TestReshaping(base.BaseReshapingTests): class TestSetitem(base.BaseSetitemTests): pass + + +# NDArrayBacked2DTests suite added in https://github.com/pandas-dev/pandas/pull/44974 +# v1.4.0rc0 +class Test2DCompat(base.NDArrayBacked2DTests): + pass diff --git a/tests/compliance/date/test_date_compliance_1_5.py b/tests/compliance/date/test_date_compliance_1_5.py deleted file mode 100644 index e8f2c93..0000000 --- a/tests/compliance/date/test_date_compliance_1_5.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2022 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. -""" -Tests for extension interface compliance, inherited from pandas. - -See: -https://github.com/pandas-dev/pandas/blob/main/pandas/tests/extension/decimal/test_decimal.py -and -https://github.com/pandas-dev/pandas/blob/main/pandas/tests/extension/test_period.py -""" - -from pandas.tests.extension import base -import pytest - -# NDArrayBacked2DTests suite added in https://github.com/pandas-dev/pandas/pull/44974 -pytest.importorskip("pandas", minversion="1.5.0dev") - - -class Test2DCompat(base.NDArrayBacked2DTests): - pass diff --git a/tests/compliance/time/test_time_compliance.py b/tests/compliance/time/test_time_compliance.py index f894ba5..118c61d 100644 --- a/tests/compliance/time/test_time_compliance.py +++ b/tests/compliance/time/test_time_compliance.py @@ -34,6 +34,11 @@ # compliance tests for reduction operations. +# NDArrayBacked2DTests suite added in https://github.com/pandas-dev/pandas/pull/44974 +class Test2DCompat(base.NDArrayBacked2DTests): + pass + + class TestComparisonOps(base.BaseComparisonOpsTests): pass diff --git a/tests/compliance/time/test_time_compliance_1_5.py b/tests/compliance/time/test_time_compliance_1_5.py deleted file mode 100644 index e8f2c93..0000000 --- a/tests/compliance/time/test_time_compliance_1_5.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2022 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. -""" -Tests for extension interface compliance, inherited from pandas. - -See: -https://github.com/pandas-dev/pandas/blob/main/pandas/tests/extension/decimal/test_decimal.py -and -https://github.com/pandas-dev/pandas/blob/main/pandas/tests/extension/test_period.py -""" - -from pandas.tests.extension import base -import pytest - -# NDArrayBacked2DTests suite added in https://github.com/pandas-dev/pandas/pull/44974 -pytest.importorskip("pandas", minversion="1.5.0dev") - - -class Test2DCompat(base.NDArrayBacked2DTests): - pass diff --git a/tests/unit/test_dtypes.py b/tests/unit/test_dtypes.py index 87b6a92..381a580 100644 --- a/tests/unit/test_dtypes.py +++ b/tests/unit/test_dtypes.py @@ -14,14 +14,11 @@ import datetime -import packaging.version import pytest 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 +535,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(): diff --git a/tests/unit/test_json.py b/tests/unit/test_json.py index d15cfc7..6b1aaa6 100644 --- a/tests/unit/test_json.py +++ b/tests/unit/test_json.py @@ -20,6 +20,8 @@ import pytest import db_dtypes +import db_dtypes.json + # Check for minimum Pandas version. pytest.importorskip("pandas", minversion="1.5.0") diff --git a/tests/unit/test_pandas_backports.py b/tests/unit/test_pandas_backports.py index eb68b6a..cb78304 100644 --- a/tests/unit/test_pandas_backports.py +++ b/tests/unit/test_pandas_backports.py @@ -35,3 +35,19 @@ 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