From ac2e42de815d6ed2c80ec0a40d4f4fafa60b7bba Mon Sep 17 00:00:00 2001 From: theOehrly <23384863+theOehrly@users.noreply.github.com> Date: Tue, 23 Jan 2024 18:48:48 +0100 Subject: [PATCH] MNT: fix deprecated passing block manager to frame or series --- fastf1/internals/pandas_base.py | 32 +++++++++++++++++--- fastf1/tests/test_internals.py | 53 ++++++++++++++++++++++++++++++++- pytest.ini | 4 +-- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/fastf1/internals/pandas_base.py b/fastf1/internals/pandas_base.py index f21d3f628..39f01f0de 100644 --- a/fastf1/internals/pandas_base.py +++ b/fastf1/internals/pandas_base.py @@ -8,6 +8,23 @@ import pandas as pd +# dangerous import of pandas internals +# these imports are covered by +# test_internals.py::test_pandas_base_internal_imports +# to detect any failures as soon as possible +try: + from pandas.core.internals import SingleBlockManager +except ImportError as exc: + _mgr_instance = getattr(pd.Series(dtype=float), '_mgr') + if _mgr_instance is None: + raise ImportError("Import of Pandas internals failed. You are likely " + "using a recently released version of Pandas that " + "isn't yet supported. Please report this issue. In" + "the meantime, you can try downgrading to an older " + "version of Pandas.") from exc + SingleBlockManager = type(_mgr_instance) + + class BaseDataFrame(pd.DataFrame): """Base class for objects that inherit from ``pandas.DataFrame``. @@ -78,11 +95,13 @@ class that implements horizontal and vertical slicing of Pandas DataFrames __meta_created_from: Optional[BaseDataFrame] def __new__(cls, data=None, index=None, *args, **kwargs) -> pd.Series: - parent = getattr(cls, '__meta_created_from') + parent = getattr(cls, '__meta_created_from', None) - if index is None: + if ((index is None) and isinstance(data, (pd.Series, + pd.DataFrame, + SingleBlockManager))): # no index is explicitly given, try to get an index from the - # data itself (for example, if `data` is a BlockManager) + # data itself index = getattr(data, 'index', None) if (parent is None) or (index is None): @@ -97,7 +116,12 @@ def __new__(cls, data=None, index=None, *args, **kwargs) -> pd.Series: # the data is a row of the parent DataFrame constructor = parent._constructor_sliced_horizontal - obj = constructor(data=data, index=index, *args, **kwargs) + if (isinstance(data, SingleBlockManager) + and hasattr(constructor, '_from_mgr') + and pd.__version__.startswith('2.')): + obj = constructor._from_mgr(data, axes=data.axes) + else: + obj = constructor(data=data, index=index, *args, **kwargs) if parent is not None: # catch-all fix for some missing __finalize__ calls in Pandas diff --git a/fastf1/tests/test_internals.py b/fastf1/tests/test_internals.py index 3b9f1ecd7..5407a6cd7 100644 --- a/fastf1/tests/test_internals.py +++ b/fastf1/tests/test_internals.py @@ -1,13 +1,24 @@ import numpy as np import pandas as pd +import pytest from fastf1.internals.pandas_base import ( BaseDataFrame, - BaseSeries + BaseSeries, + _BaseSeriesConstructor ) from fastf1.internals.pandas_extensions import _unsafe_create_df_fast +def test_pandas_base_internal_imports(): + # ensure that the internal import (still) works and ensure that the + # fallback resolves to the same type + from pandas.core.internals import SingleBlockManager + + FallbackSingleBlockManager = type(getattr(pd.Series(dtype=float), '_mgr')) + assert SingleBlockManager is FallbackSingleBlockManager + + def test_fast_df_creation(): data = {'A': [1, 2, 3], 'B': [1.0, 2.0, 3.0], 3: ['a', 'b', 'c']} @@ -24,6 +35,30 @@ def test_fast_df_creation(): pd.testing.assert_frame_equal(df_safe, df_fast) +def test_base_frame_slicing_default(): + class TestDataFrame(BaseDataFrame): + pass + + df = TestDataFrame({'A': [10, 11, 12], 'B': [20, 21, 22]}) + assert isinstance(df, TestDataFrame) + assert isinstance(df, pd.DataFrame) + + df_sliced = df.iloc[0:2] + assert isinstance(df_sliced, TestDataFrame) + assert isinstance(df_sliced, pd.DataFrame) + assert (df_sliced + == pd.DataFrame({'A': [10, 11], 'B': [20, 21]}) + ).all().all() + + vert_ser = df.loc[:, 'A'] + assert isinstance(vert_ser, pd.Series) + assert (vert_ser == pd.Series([10, 11, 12])).all() + + hor_ser = df.iloc[0] + assert isinstance(hor_ser, pd.Series) + assert (hor_ser == pd.Series({'A': 10, 'B': 20})).all() + + def test_base_frame_slicing(): class TestSeriesVertical(pd.Series): pass @@ -128,3 +163,19 @@ class TestSeries(BaseSeries): series.some_value = 100 ser_sliced = series.iloc[0:2] assert ser_sliced.some_value == 100 + + +@pytest.mark.parametrize( + "test_data", ([0, 1, 2, 3], pd.Series([0, 1, 2, 3])) +) +def test_base_series_constructor_direct_fallback(test_data): + # If for whatever reason the _BaseSeriesConstructor is not called as + # intended as _constructor_sliced from a BaseDataFrame but instead + # unplanned from anywhere else, it should fall back to behaving as if + # a pandas.Series object is created directly. + series_a = _BaseSeriesConstructor(test_data) + series_b = pd.Series(test_data) + + assert not isinstance(series_a, _BaseSeriesConstructor) + assert isinstance(series_a, pd.Series) + assert (series_a == series_b).all() diff --git a/pytest.ini b/pytest.ini index e621ee451..4369b54c7 100644 --- a/pytest.ini +++ b/pytest.ini @@ -43,6 +43,6 @@ filterwarnings = # external, new required dependency for Pandas # (01/2024) ignore:(?s).*Pyarrow will become a required dependency of pandas.*:DeprecationWarning - # external, large internal change required - # schedule for FastF1 3.3.0 (TODO) + # external, only relevant for pandas 2.2.1 due to incorrect deprecation warning + # (01/2024) ignore:Passing a (Single)?BlockManager to.*:DeprecationWarning