Skip to content

feat: support inplace=True in rename and rename_axis #1744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions bigframes/core/compile/polars/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ def _(
return args[0] < args[1]
if isinstance(op, ops.eq_op.__class__):
return args[0] == args[1]
if isinstance(op, ops.ne_op.__class__):
return args[0] != args[1]
if isinstance(op, ops.mod_op.__class__):
return args[0] % args[1]
if isinstance(op, ops.coalesce_op.__class__):
Expand All @@ -101,6 +103,9 @@ def _(
for pred, result in zip(args[2::2], args[3::2]):
return expr.when(pred).then(result)
return expr
if isinstance(op, ops.where_op.__class__):
original, condition, otherwise = args
return pl.when(condition).then(original).otherwise(otherwise)
raise NotImplementedError(f"Polars compiler hasn't implemented {op}")

@dataclasses.dataclass(frozen=True)
Expand Down
54 changes: 45 additions & 9 deletions bigframes/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,7 @@ def names(self) -> typing.Sequence[blocks.Label]:

@names.setter
def names(self, values: typing.Sequence[blocks.Label]):
new_block = self._block.with_index_labels(values)
if self._linked_frame is not None:
self._linked_frame._set_block(
self._linked_frame._block.with_index_labels(values)
)
self._block = new_block
self.rename(values, inplace=True)

@property
def nlevels(self) -> int:
Expand Down Expand Up @@ -411,11 +406,52 @@ def fillna(self, value=None) -> Index:
ops.fillna_op.as_expr(ex.free_var("arg"), ex.const(value))
)

def rename(self, name: Union[str, Sequence[str]]) -> Index:
names = [name] if isinstance(name, str) else list(name)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this change to Label (Hashable) is what's breaking some MultiIndex tests. Need a better way to check if something is already a sequence / iterable and not a string.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 45f9232

I think we want to support integer names, too, so instead I specifically exclude tuple from the check for hashable objects.

@overload
def rename(
self,
name: Union[blocks.Label, Sequence[blocks.Label]],
) -> Index:
...

@overload
def rename(
self,
name: Union[blocks.Label, Sequence[blocks.Label]],
*,
inplace: Literal[False],
) -> Index:
...

@overload
def rename(
self,
name: Union[blocks.Label, Sequence[blocks.Label]],
*,
inplace: Literal[True],
) -> None:
...

def rename(
self,
name: Union[blocks.Label, Sequence[blocks.Label]],
*,
inplace: bool = False,
) -> Optional[Index]:
names = [name] if isinstance(name, blocks.Label) else list(name)
if len(names) != self.nlevels:
raise ValueError("'name' must be same length as levels")
return Index(self._block.with_index_labels(names))

new_block = self._block.with_index_labels(names)

if inplace:
if self._linked_frame is not None:
self._linked_frame._set_block(
self._linked_frame._block.with_index_labels(names)
)
self._block = new_block
return None
else:
return Index(new_block)

def drop(
self,
Expand Down
63 changes: 61 additions & 2 deletions bigframes/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -2081,15 +2081,67 @@ def reorder_levels(self, order: LevelsType, axis: int | str = 0):
def _resolve_levels(self, level: LevelsType) -> typing.Sequence[str]:
return self._block.index.resolve_level(level)

@overload
def rename(self, *, columns: Mapping[blocks.Label, blocks.Label]) -> DataFrame:
...

@overload
def rename(
self, *, columns: Mapping[blocks.Label, blocks.Label], inplace: Literal[False]
) -> DataFrame:
...

@overload
def rename(
self, *, columns: Mapping[blocks.Label, blocks.Label], inplace: Literal[True]
) -> None:
...

def rename(
self, *, columns: Mapping[blocks.Label, blocks.Label], inplace: bool = False
) -> Optional[DataFrame]:
block = self._block.rename(columns=columns)
return DataFrame(block)

if inplace:
self._block = block
return None
else:
return DataFrame(block)

@overload
def rename_axis(
self,
mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]],
) -> DataFrame:
...

@overload
def rename_axis(
self,
mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]],
*,
inplace: Literal[False],
**kwargs,
) -> DataFrame:
...

@overload
def rename_axis(
self,
mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]],
*,
inplace: Literal[True],
**kwargs,
) -> None:
...

def rename_axis(
self,
mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]],
*,
inplace: bool = False,
**kwargs,
) -> Optional[DataFrame]:
if len(kwargs) != 0:
raise NotImplementedError(
f"rename_axis does not currently support any keyword arguments. {constants.FEEDBACK_LINK}"
Expand All @@ -2099,7 +2151,14 @@ def rename_axis(
labels = mapper
else:
labels = [mapper]
return DataFrame(self._block.with_index_labels(labels))

block = self._block.with_index_labels(labels)

if inplace:
self._block = block
return None
else:
return DataFrame(block)

@validations.requires_ordering()
def equals(self, other: typing.Union[bigframes.series.Series, DataFrame]) -> bool:
Expand Down
99 changes: 86 additions & 13 deletions bigframes/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
Literal,
Mapping,
Optional,
overload,
Sequence,
Tuple,
Union,
Expand Down Expand Up @@ -94,6 +95,10 @@ class Series(bigframes.operations.base.SeriesMethods, vendored_pandas_series.Ser
# Must be above 5000 for pandas to delegate to bigframes for binops
__pandas_priority__ = 13000

# Ensure mypy can more robustly determine the type of self._block since it
# gets set in various places.
_block: blocks.Block

def __init__(self, *args, **kwargs):
self._query_job: Optional[bigquery.QueryJob] = None
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -253,22 +258,45 @@ def __iter__(self) -> typing.Iterator:
def copy(self) -> Series:
return Series(self._block)

@overload
def rename(
self, index: Union[blocks.Label, Mapping[Any, Any]] = None, **kwargs
self,
index: Union[blocks.Label, Mapping[Any, Any]] = None,
) -> Series:
...

@overload
def rename(
self,
index: Union[blocks.Label, Mapping[Any, Any]] = None,
*,
inplace: Literal[False],
**kwargs,
) -> Series:
...

@overload
def rename(
self,
index: Union[blocks.Label, Mapping[Any, Any]] = None,
*,
inplace: Literal[True],
**kwargs,
) -> None:
...

def rename(
self,
index: Union[blocks.Label, Mapping[Any, Any]] = None,
*,
inplace: bool = False,
**kwargs,
) -> Optional[Series]:
if len(kwargs) != 0:
raise NotImplementedError(
f"rename does not currently support any keyword arguments. {constants.FEEDBACK_LINK}"
)

# rename the Series name
if index is None or isinstance(
index, str
): # Python 3.9 doesn't allow isinstance of Optional
index = typing.cast(Optional[str], index)
block = self._block.with_column_labels([index])
return Series(block)

# rename the index
if isinstance(index, Mapping):
index = typing.cast(Mapping[Any, Any], index)
Expand All @@ -293,22 +321,61 @@ def rename(

block = block.set_index(new_idx_ids, index_labels=block.index.names)

return Series(block)
if inplace:
self._block = block
return None
else:
return Series(block)

# rename the Series name
if isinstance(index, typing.Hashable):
# Python 3.9 doesn't allow isinstance of Optional
index = typing.cast(Optional[str], index)
block = self._block.with_column_labels([index])
return Series(block)

if inplace:
self._block = block
return None
else:
return Series(block)

raise ValueError(f"Unsupported type of parameter index: {type(index)}")

@validations.requires_index
@overload
def rename_axis(
self,
mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]],
) -> Series:
...

@overload
def rename_axis(
self,
mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]],
*,
inplace: Literal[False],
**kwargs,
) -> Series:
...

@overload
def rename_axis(
self,
mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]],
*,
inplace: Literal[True],
**kwargs,
) -> None:
...

@validations.requires_index
def rename_axis(
self,
mapper: typing.Union[blocks.Label, typing.Sequence[blocks.Label]],
*,
inplace: bool = False,
**kwargs,
) -> Optional[Series]:
if len(kwargs) != 0:
raise NotImplementedError(
f"rename_axis does not currently support any keyword arguments. {constants.FEEDBACK_LINK}"
Expand All @@ -318,7 +385,13 @@ def rename_axis(
labels = mapper
else:
labels = [mapper]
return Series(self._block.with_index_labels(labels))

block = self._block.with_index_labels(labels)
if inplace:
self._block = block
return None
else:
return Series(block)

def equals(
self, other: typing.Union[Series, bigframes.dataframe.DataFrame]
Expand Down
17 changes: 10 additions & 7 deletions bigframes/testing/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import copy
import datetime
from typing import Optional, Sequence
from typing import Any, Dict, Optional, Sequence
import unittest.mock as mock

import google.auth.credentials
Expand All @@ -23,12 +23,9 @@

import bigframes
import bigframes.clients
import bigframes.core.ordering
import bigframes.core.global_session
import bigframes.dataframe
import bigframes.series
import bigframes.session.clients
import bigframes.session.executor
import bigframes.session.metrics

"""Utilities for creating test resources."""

Expand Down Expand Up @@ -129,7 +126,10 @@ def query_and_wait_mock(query, *args, job_config=None, **kwargs):


def create_dataframe(
monkeypatch: pytest.MonkeyPatch, *, session: Optional[bigframes.Session] = None
monkeypatch: pytest.MonkeyPatch,
*,
session: Optional[bigframes.Session] = None,
data: Optional[Dict[str, Sequence[Any]]] = None,
) -> bigframes.dataframe.DataFrame:
"""[Experimental] Create a mock DataFrame that avoids making Google Cloud API calls.

Expand All @@ -138,8 +138,11 @@ def create_dataframe(
if session is None:
session = create_bigquery_session()

if data is None:
data = {"col": []}

# Since this may create a ReadLocalNode, the session we explicitly pass in
# might not actually be used. Mock out the global session, too.
monkeypatch.setattr(bigframes.core.global_session, "_global_session", session)
bigframes.options.bigquery._session_started = True
return bigframes.dataframe.DataFrame({"col": []}, session=session)
return bigframes.dataframe.DataFrame(data, session=session)
3 changes: 3 additions & 0 deletions tests/unit/polars_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from typing import Optional, Union
import weakref

import pandas
import polars

import bigframes
Expand Down Expand Up @@ -87,5 +88,7 @@ def __init__(self):

def read_pandas(self, pandas_dataframe, write_engine="default"):
# override read_pandas to always keep data local-only
if isinstance(pandas_dataframe, pandas.Series):
pandas_dataframe = pandas_dataframe.to_frame()
local_block = bigframes.core.blocks.Block.from_local(pandas_dataframe, self)
return bigframes.dataframe.DataFrame(local_block)
Loading