Skip to content

Commit

Permalink
MNT: fix deprecated passing block manager to frame or series
Browse files Browse the repository at this point in the history
  • Loading branch information
theOehrly committed Feb 6, 2024
1 parent ad9b1ee commit ac2e42d
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 7 deletions.
32 changes: 28 additions & 4 deletions fastf1/internals/pandas_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down
53 changes: 52 additions & 1 deletion fastf1/tests/test_internals.py
Original file line number Diff line number Diff line change
@@ -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']}

Expand All @@ -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
Expand Down Expand Up @@ -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()
4 changes: 2 additions & 2 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit ac2e42d

Please sign in to comment.