Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@

import narwhals as nw
from narwhals._utils import Implementation, generate_temporary_column_name
from tests.utils import ID_PANDAS_LIKE, PANDAS_VERSION, pyspark_session, sqlframe_session
from tests.utils import (
ID_PANDAS_LIKE,
PANDAS_VERSION,
any_integer_like_floats,
pyspark_session,
sqlframe_session,
)

if TYPE_CHECKING:
from collections.abc import Callable, Sequence
Expand Down Expand Up @@ -102,14 +108,29 @@ def pandas_constructor(obj: Data) -> pd.DataFrame:
def pandas_nullable_constructor(obj: Data) -> pd.DataFrame:
import pandas as pd

return pd.DataFrame(obj).convert_dtypes(dtype_backend="numpy_nullable")
return pd.DataFrame(
{
k: pd.Series(v).convert_dtypes(
convert_integer=not any_integer_like_floats(v),
dtype_backend="numpy_nullable",
)
for k, v in obj.items()
}
)


def pandas_pyarrow_constructor(obj: Data) -> pd.DataFrame:
pytest.importorskip("pyarrow")
import pandas as pd

return pd.DataFrame(obj).convert_dtypes(dtype_backend="pyarrow")
return pd.DataFrame(
{
k: pd.Series(v).convert_dtypes(
convert_integer=not any_integer_like_floats(v), dtype_backend="pyarrow"
)
for k, v in obj.items()
}
)


def modin_constructor(obj: Data) -> IntoDataFrame: # pragma: no cover
Expand All @@ -124,7 +145,14 @@ def modin_pyarrow_constructor(obj: Data) -> IntoDataFrame: # pragma: no cover
import modin.pandas as mpd
import pandas as pd

df = mpd.DataFrame(pd.DataFrame(obj)).convert_dtypes(dtype_backend="pyarrow")
df = mpd.DataFrame(
{
k: pd.Series(v).convert_dtypes(
convert_integer=not any_integer_like_floats(v), dtype_backend="pyarrow"
)
for k, v in obj.items()
}
)
return cast("IntoDataFrame", df)


Expand Down
4 changes: 3 additions & 1 deletion tests/frame/to_pandas_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ def test_convert_pandas(constructor_eager: ConstructorEager) -> None:
if constructor_eager.__name__.startswith("pandas"):
expected = cast("pd.DataFrame", constructor_eager(data))
elif "modin_pyarrow" in str(constructor_eager):
expected = pd.DataFrame(data).convert_dtypes(dtype_backend="pyarrow")
from modin.pandas.io import to_pandas

expected = to_pandas(constructor_eager(data))
else:
expected = pd.DataFrame(data)

Expand Down
5 changes: 5 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,8 @@ def xfail_if_pyspark_connect( # pragma: no cover
) -> None:
if is_pyspark_connect(constructor):
request.applymarker(pytest.mark.xfail(reason=reason))


def any_integer_like_floats(values: list[Any]) -> bool:
"""Return True if any value is a float that represents an integer (e.g. 1.0, 2.0)."""
return any(isinstance(v, float) and v.is_integer() for v in values)
34 changes: 33 additions & 1 deletion tests/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
import string
from dataclasses import dataclass
from datetime import date, datetime
from itertools import chain
from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast

Expand All @@ -19,7 +20,7 @@
parse_version,
requires,
)
from tests.utils import get_module_version_as_tuple
from tests.utils import any_integer_like_floats, get_module_version_as_tuple

pytest.importorskip("pandas")
import pandas as pd
Expand Down Expand Up @@ -626,3 +627,34 @@ def fn() -> Iterator[str]:
assert next(iter(deferred_1)) == "h"
assert list(deferred_1) == list("hello")
assert "".join(chain(deferred_1, deferred_2)) == "helloHELLO"


@pytest.mark.parametrize(
("values", "expected"),
[
pytest.param([1, 2, 3], False, id="all_ints"),
pytest.param([1.5, 2.2, 3.1], False, id="no_integer_like_floats"),
pytest.param([1.0, 2.0, 3.5], True, id="some_integer_like_floats"),
pytest.param([1.0, 2.0, 3.0], True, id="all_integer_like_floats"),
pytest.param([1, 2.0, 3], True, id="mixed_int_and_float"),
pytest.param([], False, id="empty_list"),
pytest.param([-1.0, -2.0], True, id="negative_integer_like_floats"),
pytest.param([1.0, 2.5, 3.0, 4.1], True, id="mixed_floats"),
# mixed non-numeric types
pytest.param(["a", "b", "c"], False, id="all_strings"),
pytest.param([1.0, "a", 2.0], True, id="floats_and_strings"),
pytest.param([1, "a", 2], False, id="ints_and_strings_no_floats"),
# datetime / date objects
pytest.param([date(2024, 1, 1), date(2024, 1, 2)], False, id="all_dates"),
pytest.param(
[datetime(2024, 1, 1), 1.0, datetime(2024, 1, 2)],
True,
id="floats_and_datetimes",
),
# None / null handling
pytest.param([None, None], False, id="all_none"),
pytest.param([1.0, None, 2.0], True, id="floats_and_none"),
],
)
def test_any_integer_like_floats(values: list[Any], expected: bool) -> None: # noqa: FBT001
assert any_integer_like_floats(values) is expected
Loading