diff --git a/bigframes/_importing.py b/bigframes/_importing.py index 095a1d9c51..e88bd77fe8 100644 --- a/bigframes/_importing.py +++ b/bigframes/_importing.py @@ -14,6 +14,7 @@ import importlib from types import ModuleType +import numpy from packaging import version # Keep this in sync with setup.py @@ -22,9 +23,13 @@ def import_polars() -> ModuleType: polars_module = importlib.import_module("polars") - imported_version = version.Version(polars_module.build_info()["version"]) - if imported_version < POLARS_MIN_VERSION: + # Check for necessary methods instead of the version number because we + # can't trust the polars version until + # https://github.com/pola-rs/polars/issues/23940 is fixed. + try: + polars_module.lit(numpy.int64(100), dtype=polars_module.Int64()) + except TypeError: raise ImportError( - f"Imported polars version: {imported_version} is below the minimum version: {POLARS_MIN_VERSION}" + f"Imported polars version is likely below the minimum version: {POLARS_MIN_VERSION}" ) return polars_module diff --git a/bigframes/core/compile/__init__.py b/bigframes/core/compile/__init__.py index e2487306ab..68c36df288 100644 --- a/bigframes/core/compile/__init__.py +++ b/bigframes/core/compile/__init__.py @@ -14,8 +14,8 @@ from __future__ import annotations from bigframes.core.compile.api import test_only_ibis_inferred_schema -from bigframes.core.compile.compiler import compile_sql from bigframes.core.compile.configs import CompileRequest, CompileResult +from bigframes.core.compile.ibis_compiler.ibis_compiler import compile_sql __all__ = [ "test_only_ibis_inferred_schema", diff --git a/bigframes/core/compile/api.py b/bigframes/core/compile/api.py index ddd8622327..3a4695c50d 100644 --- a/bigframes/core/compile/api.py +++ b/bigframes/core/compile/api.py @@ -16,7 +16,7 @@ from typing import TYPE_CHECKING from bigframes.core import rewrite -from bigframes.core.compile import compiler +from bigframes.core.compile.ibis_compiler import ibis_compiler if TYPE_CHECKING: import bigframes.core.nodes @@ -26,9 +26,9 @@ def test_only_ibis_inferred_schema(node: bigframes.core.nodes.BigFrameNode): """Use only for testing paths to ensure ibis inferred schema does not diverge from bigframes inferred schema.""" import bigframes.core.schema - node = compiler._replace_unsupported_ops(node) + node = ibis_compiler._replace_unsupported_ops(node) node = rewrite.bake_order(node) - ir = compiler.compile_node(node) + ir = ibis_compiler.compile_node(node) items = tuple( bigframes.core.schema.SchemaItem(name, ir.get_column_type(ibis_id)) for name, ibis_id in zip(node.schema.names, ir.column_ids) diff --git a/bigframes/core/compile/compiled.py b/bigframes/core/compile/compiled.py index 314b54fc6d..27660d2ba7 100644 --- a/bigframes/core/compile/compiled.py +++ b/bigframes/core/compile/compiled.py @@ -30,11 +30,10 @@ import pyarrow as pa from bigframes.core import utils -import bigframes.core.compile.aggregate_compiler as agg_compiler import bigframes.core.compile.googlesql +import bigframes.core.compile.ibis_compiler.aggregate_compiler as agg_compiler +import bigframes.core.compile.ibis_compiler.scalar_op_compiler as op_compilers import bigframes.core.compile.ibis_types -import bigframes.core.compile.scalar_op_compiler as op_compilers -import bigframes.core.compile.scalar_op_compiler as scalar_op_compiler import bigframes.core.expression as ex from bigframes.core.ordering import OrderingExpression import bigframes.core.sql @@ -679,13 +678,15 @@ def _join_condition( def _as_groupable(value: ibis_types.Value): + from bigframes.core.compile.ibis_compiler import scalar_op_registry + # Some types need to be converted to another type to enable groupby if value.type().is_float64(): return value.cast(ibis_dtypes.str) elif value.type().is_geospatial(): return typing.cast(ibis_types.GeoSpatialColumn, value).as_binary() elif value.type().is_json(): - return scalar_op_compiler.to_json_string(value) + return scalar_op_registry.to_json_string(value) else: return value diff --git a/tests/system/small/pandas/io/__init__.py b/bigframes/core/compile/ibis_compiler/__init__.py similarity index 62% rename from tests/system/small/pandas/io/__init__.py rename to bigframes/core/compile/ibis_compiler/__init__.py index 0a2669d7a2..aef0ed9267 100644 --- a/tests/system/small/pandas/io/__init__.py +++ b/bigframes/core/compile/ibis_compiler/__init__.py @@ -11,3 +11,14 @@ # 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. + +"""Compiler for BigFrames expression to Ibis expression. + +Make sure to import all ibis_compiler implementations here so that they get +registered. +""" + +from __future__ import annotations + +import bigframes.core.compile.ibis_compiler.operations.generic_ops # noqa: F401 +import bigframes.core.compile.ibis_compiler.scalar_op_registry # noqa: F401 diff --git a/bigframes/core/compile/aggregate_compiler.py b/bigframes/core/compile/ibis_compiler/aggregate_compiler.py similarity index 99% rename from bigframes/core/compile/aggregate_compiler.py rename to bigframes/core/compile/ibis_compiler/aggregate_compiler.py index 0d31798f25..4e0bf477fc 100644 --- a/bigframes/core/compile/aggregate_compiler.py +++ b/bigframes/core/compile/ibis_compiler/aggregate_compiler.py @@ -27,8 +27,8 @@ import pandas as pd from bigframes.core.compile import constants as compiler_constants +import bigframes.core.compile.ibis_compiler.scalar_op_compiler as scalar_compilers import bigframes.core.compile.ibis_types as compile_ibis_types -import bigframes.core.compile.scalar_op_compiler as scalar_compilers import bigframes.core.expression as ex import bigframes.core.window_spec as window_spec import bigframes.operations.aggregations as agg_ops diff --git a/bigframes/core/compile/compiler.py b/bigframes/core/compile/ibis_compiler/ibis_compiler.py similarity index 98% rename from bigframes/core/compile/compiler.py rename to bigframes/core/compile/ibis_compiler/ibis_compiler.py index 0efbd47ae4..ff0441ea22 100644 --- a/bigframes/core/compile/compiler.py +++ b/bigframes/core/compile/ibis_compiler/ibis_compiler.py @@ -29,7 +29,6 @@ import bigframes.core.compile.concat as concat_impl import bigframes.core.compile.configs as configs import bigframes.core.compile.explode -import bigframes.core.compile.scalar_op_compiler as compile_scalar import bigframes.core.nodes as nodes import bigframes.core.ordering as bf_ordering import bigframes.core.rewrite as rewrites @@ -178,6 +177,8 @@ def compile_readlocal(node: nodes.ReadLocalNode, *args): @_compile_node.register def compile_readtable(node: nodes.ReadTableNode, *args): + from bigframes.core.compile.ibis_compiler import scalar_op_registry + ibis_table = _table_to_ibis( node.source, scan_cols=[col.source_id for col in node.scan_list.items] ) @@ -188,7 +189,7 @@ def compile_readtable(node: nodes.ReadTableNode, *args): scan_item.dtype == dtypes.JSON_DTYPE and ibis_table[scan_item.source_id].type() == ibis_dtypes.string ): - json_column = compile_scalar.parse_json( + json_column = scalar_op_registry.parse_json( ibis_table[scan_item.source_id] ).name(scan_item.source_id) ibis_table = ibis_table.mutate(json_column) diff --git a/tests/system/small/pandas/io/api/__init__.py b/bigframes/core/compile/ibis_compiler/operations/__init__.py similarity index 67% rename from tests/system/small/pandas/io/api/__init__.py rename to bigframes/core/compile/ibis_compiler/operations/__init__.py index 0a2669d7a2..9d9f3849ab 100644 --- a/tests/system/small/pandas/io/api/__init__.py +++ b/bigframes/core/compile/ibis_compiler/operations/__init__.py @@ -11,3 +11,11 @@ # 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. + +"""Operation implementations for the Ibis-based compiler. + +This directory structure should reflect the same layout as the +`bigframes/operations` directory where the operations are defined. + +Prefer a few ops per file to keep file sizes manageable for text editors and LLMs. +""" diff --git a/bigframes/core/compile/ibis_compiler/operations/generic_ops.py b/bigframes/core/compile/ibis_compiler/operations/generic_ops.py new file mode 100644 index 0000000000..78f6a0c4de --- /dev/null +++ b/bigframes/core/compile/ibis_compiler/operations/generic_ops.py @@ -0,0 +1,38 @@ +# Copyright 2025 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. + +""" +BigFrames -> Ibis compilation for the operations in bigframes.operations.generic_ops. + +Please keep implementations in sequential order by op name. +""" + +from __future__ import annotations + +from bigframes_vendored.ibis.expr import types as ibis_types + +from bigframes.core.compile.ibis_compiler import scalar_op_compiler +from bigframes.operations import generic_ops + +register_unary_op = scalar_op_compiler.scalar_op_compiler.register_unary_op + + +@register_unary_op(generic_ops.notnull_op) +def notnull_op_impl(x: ibis_types.Value): + return x.notnull() + + +@register_unary_op(generic_ops.isnull_op) +def isnull_op_impl(x: ibis_types.Value): + return x.isnull() diff --git a/bigframes/core/compile/ibis_compiler/scalar_op_compiler.py b/bigframes/core/compile/ibis_compiler/scalar_op_compiler.py new file mode 100644 index 0000000000..d5f3e15d34 --- /dev/null +++ b/bigframes/core/compile/ibis_compiler/scalar_op_compiler.py @@ -0,0 +1,207 @@ +# Copyright 2023 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. + +"""To avoid circular imports, this module should _not_ depend on any ops.""" + +from __future__ import annotations + +import functools +import typing +from typing import TYPE_CHECKING + +import bigframes_vendored.ibis.expr.types as ibis_types + +import bigframes.core.compile.ibis_types +import bigframes.core.expression as ex + +if TYPE_CHECKING: + import bigframes.operations as ops + + +class ScalarOpCompiler: + # Mapping of operation name to implemenations + _registry: dict[ + str, + typing.Callable[ + [typing.Sequence[ibis_types.Value], ops.RowOp], ibis_types.Value + ], + ] = {} + + @functools.singledispatchmethod + def compile_expression( + self, + expression: ex.Expression, + bindings: typing.Dict[str, ibis_types.Value], + ) -> ibis_types.Value: + raise NotImplementedError(f"Unrecognized expression: {expression}") + + @compile_expression.register + def _( + self, + expression: ex.ScalarConstantExpression, + bindings: typing.Dict[str, ibis_types.Value], + ) -> ibis_types.Value: + return bigframes.core.compile.ibis_types.literal_to_ibis_scalar( + expression.value, expression.dtype + ) + + @compile_expression.register + def _( + self, + expression: ex.DerefOp, + bindings: typing.Dict[str, ibis_types.Value], + ) -> ibis_types.Value: + if expression.id.sql not in bindings: + raise ValueError(f"Could not resolve unbound variable {expression.id}") + else: + return bindings[expression.id.sql] + + @compile_expression.register + def _( + self, + expression: ex.OpExpression, + bindings: typing.Dict[str, ibis_types.Value], + ) -> ibis_types.Value: + inputs = [ + self.compile_expression(sub_expr, bindings) + for sub_expr in expression.inputs + ] + return self.compile_row_op(expression.op, inputs) + + def compile_row_op( + self, op: ops.RowOp, inputs: typing.Sequence[ibis_types.Value] + ) -> ibis_types.Value: + impl = self._registry[op.name] + return impl(inputs, op) + + def register_unary_op( + self, + op_ref: typing.Union[ops.UnaryOp, type[ops.UnaryOp]], + pass_op: bool = False, + ): + """ + Decorator to register a unary op implementation. + + Args: + op_ref (UnaryOp or UnaryOp type): + Class or instance of operator that is implemented by the decorated function. + pass_op (bool): + Set to true if implementation takes the operator object as the last argument. + This is needed for parameterized ops where parameters are part of op object. + """ + key = typing.cast(str, op_ref.name) + + def decorator(impl: typing.Callable[..., ibis_types.Value]): + def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): + if pass_op: + return impl(args[0], op) + else: + return impl(args[0]) + + self._register(key, normalized_impl) + return impl + + return decorator + + def register_binary_op( + self, + op_ref: typing.Union[ops.BinaryOp, type[ops.BinaryOp]], + pass_op: bool = False, + ): + """ + Decorator to register a binary op implementation. + + Args: + op_ref (BinaryOp or BinaryOp type): + Class or instance of operator that is implemented by the decorated function. + pass_op (bool): + Set to true if implementation takes the operator object as the last argument. + This is needed for parameterized ops where parameters are part of op object. + """ + key = typing.cast(str, op_ref.name) + + def decorator(impl: typing.Callable[..., ibis_types.Value]): + def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): + if pass_op: + return impl(args[0], args[1], op) + else: + return impl(args[0], args[1]) + + self._register(key, normalized_impl) + return impl + + return decorator + + def register_ternary_op( + self, op_ref: typing.Union[ops.TernaryOp, type[ops.TernaryOp]] + ): + """ + Decorator to register a ternary op implementation. + + Args: + op_ref (TernaryOp or TernaryOp type): + Class or instance of operator that is implemented by the decorated function. + """ + key = typing.cast(str, op_ref.name) + + def decorator(impl: typing.Callable[..., ibis_types.Value]): + def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): + return impl(args[0], args[1], args[2]) + + self._register(key, normalized_impl) + return impl + + return decorator + + def register_nary_op( + self, op_ref: typing.Union[ops.NaryOp, type[ops.NaryOp]], pass_op: bool = False + ): + """ + Decorator to register a nary op implementation. + + Args: + op_ref (NaryOp or NaryOp type): + Class or instance of operator that is implemented by the decorated function. + pass_op (bool): + Set to true if implementation takes the operator object as the last argument. + This is needed for parameterized ops where parameters are part of op object. + """ + key = typing.cast(str, op_ref.name) + + def decorator(impl: typing.Callable[..., ibis_types.Value]): + def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): + if pass_op: + return impl(*args, op=op) + else: + return impl(*args) + + self._register(key, normalized_impl) + return impl + + return decorator + + def _register( + self, + op_name: str, + impl: typing.Callable[ + [typing.Sequence[ibis_types.Value], ops.RowOp], ibis_types.Value + ], + ): + if op_name in self._registry: + raise ValueError(f"Operation name {op_name} already registered") + self._registry[op_name] = impl + + +# Singleton compiler +scalar_op_compiler = ScalarOpCompiler() diff --git a/bigframes/core/compile/scalar_op_compiler.py b/bigframes/core/compile/ibis_compiler/scalar_op_registry.py similarity index 92% rename from bigframes/core/compile/scalar_op_compiler.py rename to bigframes/core/compile/ibis_compiler/scalar_op_registry.py index c3430120cf..bc077c1ce3 100644 --- a/bigframes/core/compile/scalar_op_compiler.py +++ b/bigframes/core/compile/ibis_compiler/scalar_op_registry.py @@ -27,9 +27,10 @@ from bigframes.core.compile.constants import UNIT_TO_US_CONVERSION_FACTORS import bigframes.core.compile.default_ordering +from bigframes.core.compile.ibis_compiler.scalar_op_compiler import ( + scalar_op_compiler, # TODO(tswast): avoid import of variables +) import bigframes.core.compile.ibis_types -import bigframes.core.expression as ex -import bigframes.dtypes import bigframes.operations as ops _ZERO = typing.cast(ibis_types.NumericValue, ibis_types.literal(0)) @@ -51,195 +52,7 @@ _OBJ_REF_IBIS_DTYPE = ibis_dtypes.Struct.from_tuples(_OBJ_REF_STRUCT_SCHEMA) # type: ignore -class ScalarOpCompiler: - # Mapping of operation name to implemenations - _registry: dict[ - str, - typing.Callable[ - [typing.Sequence[ibis_types.Value], ops.RowOp], ibis_types.Value - ], - ] = {} - - @functools.singledispatchmethod - def compile_expression( - self, - expression: ex.Expression, - bindings: typing.Dict[str, ibis_types.Value], - ) -> ibis_types.Value: - raise NotImplementedError(f"Unrecognized expression: {expression}") - - @compile_expression.register - def _( - self, - expression: ex.ScalarConstantExpression, - bindings: typing.Dict[str, ibis_types.Value], - ) -> ibis_types.Value: - return bigframes.core.compile.ibis_types.literal_to_ibis_scalar( - expression.value, expression.dtype - ) - - @compile_expression.register - def _( - self, - expression: ex.DerefOp, - bindings: typing.Dict[str, ibis_types.Value], - ) -> ibis_types.Value: - if expression.id.sql not in bindings: - raise ValueError(f"Could not resolve unbound variable {expression.id}") - else: - return bindings[expression.id.sql] - - @compile_expression.register - def _( - self, - expression: ex.OpExpression, - bindings: typing.Dict[str, ibis_types.Value], - ) -> ibis_types.Value: - inputs = [ - self.compile_expression(sub_expr, bindings) - for sub_expr in expression.inputs - ] - return self.compile_row_op(expression.op, inputs) - - def compile_row_op( - self, op: ops.RowOp, inputs: typing.Sequence[ibis_types.Value] - ) -> ibis_types.Value: - impl = self._registry[op.name] - return impl(inputs, op) - - def register_unary_op( - self, - op_ref: typing.Union[ops.UnaryOp, type[ops.UnaryOp]], - pass_op: bool = False, - ): - """ - Decorator to register a unary op implementation. - - Args: - op_ref (UnaryOp or UnaryOp type): - Class or instance of operator that is implemented by the decorated function. - pass_op (bool): - Set to true if implementation takes the operator object as the last argument. - This is needed for parameterized ops where parameters are part of op object. - """ - key = typing.cast(str, op_ref.name) - - def decorator(impl: typing.Callable[..., ibis_types.Value]): - def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): - if pass_op: - return impl(args[0], op) - else: - return impl(args[0]) - - self._register(key, normalized_impl) - return impl - - return decorator - - def register_binary_op( - self, - op_ref: typing.Union[ops.BinaryOp, type[ops.BinaryOp]], - pass_op: bool = False, - ): - """ - Decorator to register a binary op implementation. - - Args: - op_ref (BinaryOp or BinaryOp type): - Class or instance of operator that is implemented by the decorated function. - pass_op (bool): - Set to true if implementation takes the operator object as the last argument. - This is needed for parameterized ops where parameters are part of op object. - """ - key = typing.cast(str, op_ref.name) - - def decorator(impl: typing.Callable[..., ibis_types.Value]): - def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): - if pass_op: - return impl(args[0], args[1], op) - else: - return impl(args[0], args[1]) - - self._register(key, normalized_impl) - return impl - - return decorator - - def register_ternary_op( - self, op_ref: typing.Union[ops.TernaryOp, type[ops.TernaryOp]] - ): - """ - Decorator to register a ternary op implementation. - - Args: - op_ref (TernaryOp or TernaryOp type): - Class or instance of operator that is implemented by the decorated function. - """ - key = typing.cast(str, op_ref.name) - - def decorator(impl: typing.Callable[..., ibis_types.Value]): - def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): - return impl(args[0], args[1], args[2]) - - self._register(key, normalized_impl) - return impl - - return decorator - - def register_nary_op( - self, op_ref: typing.Union[ops.NaryOp, type[ops.NaryOp]], pass_op: bool = False - ): - """ - Decorator to register a nary op implementation. - - Args: - op_ref (NaryOp or NaryOp type): - Class or instance of operator that is implemented by the decorated function. - pass_op (bool): - Set to true if implementation takes the operator object as the last argument. - This is needed for parameterized ops where parameters are part of op object. - """ - key = typing.cast(str, op_ref.name) - - def decorator(impl: typing.Callable[..., ibis_types.Value]): - def normalized_impl(args: typing.Sequence[ibis_types.Value], op: ops.RowOp): - if pass_op: - return impl(*args, op=op) - else: - return impl(*args) - - self._register(key, normalized_impl) - return impl - - return decorator - - def _register( - self, - op_name: str, - impl: typing.Callable[ - [typing.Sequence[ibis_types.Value], ops.RowOp], ibis_types.Value - ], - ): - if op_name in self._registry: - raise ValueError(f"Operation name {op_name} already registered") - self._registry[op_name] = impl - - -# Singleton compiler -scalar_op_compiler = ScalarOpCompiler() - - ### Unary Ops -@scalar_op_compiler.register_unary_op(ops.isnull_op) -def isnull_op_impl(x: ibis_types.Value): - return x.isnull() - - -@scalar_op_compiler.register_unary_op(ops.notnull_op) -def notnull_op_impl(x: ibis_types.Value): - return x.notnull() - - @scalar_op_compiler.register_unary_op(ops.hash_op) def hash_op_impl(x: ibis_types.Value): return typing.cast(ibis_types.IntegerValue, x).hash() diff --git a/bigframes/core/compile/polars/__init__.py b/bigframes/core/compile/polars/__init__.py index 8c37e046ab..7ae6fcc755 100644 --- a/bigframes/core/compile/polars/__init__.py +++ b/bigframes/core/compile/polars/__init__.py @@ -11,16 +11,30 @@ # 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. + +"""Compiler for BigFrames expression to Polars LazyFrame expression. + +Make sure to import all polars implementations here so that they get registered. +""" from __future__ import annotations import warnings +# The ops imports appear first so that the implementations can be registered. +# polars shouldn't be needed at import time, as register is a no-op if polars +# isn't installed. +import bigframes.core.compile.polars.operations.generic_ops # noqa: F401 + try: - import polars # noqa + import bigframes._importing + + # Use import_polars() instead of importing directly so that we check the + # version numbers. + bigframes._importing.import_polars() from bigframes.core.compile.polars.compiler import PolarsCompiler __all__ = ["PolarsCompiler"] -except Exception: - msg = "Polars compiler not available as polars is not installed." +except Exception as exc: + msg = f"Polars compiler not available as there was an exception importing polars. Details: {str(exc)}" warnings.warn(msg) diff --git a/bigframes/core/compile/polars/compiler.py b/bigframes/core/compile/polars/compiler.py index e1531ee9e5..f3088d69a8 100644 --- a/bigframes/core/compile/polars/compiler.py +++ b/bigframes/core/compile/polars/compiler.py @@ -17,7 +17,7 @@ import functools import itertools import operator -from typing import cast, Literal, Optional, Sequence, Tuple, TYPE_CHECKING +from typing import cast, Literal, Optional, Sequence, Tuple, Type, TYPE_CHECKING import pandas as pd @@ -42,10 +42,35 @@ import polars as pl else: try: - import polars as pl + import bigframes._importing + + # Use import_polars() instead of importing directly so that we check + # the version numbers. + pl = bigframes._importing.import_polars() except Exception: polars_installed = False + +def register_op(op: Type): + """Register a compilation from BigFrames to Ibis. + + This decorator can be used, even if Polars is not installed. + + Args: + op: The type of the operator the wrapped function compiles. + """ + + def decorator(func): + if polars_installed: + # Ignore the type because compile_op is a generic Callable, so + # register isn't available according to mypy. + return PolarsExpressionCompiler.compile_op.register(op)(func) # type: ignore + else: + return func + + return decorator + + if polars_installed: _DTYPE_MAPPING = { # Direct mappings @@ -238,14 +263,6 @@ def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr: else: return input.is_in(op.values) or input.is_null() - @compile_op.register(gen_ops.IsNullOp) - def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr: - return input.is_null() - - @compile_op.register(gen_ops.NotNullOp) - def _(self, op: ops.ScalarOp, input: pl.Expr) -> pl.Expr: - return input.is_not_null() - @compile_op.register(gen_ops.FillNaOp) @compile_op.register(gen_ops.CoalesceOp) def _(self, op: ops.ScalarOp, l_input: pl.Expr, r_input: pl.Expr) -> pl.Expr: diff --git a/tests/system/small/pandas/core/methods/__init__.py b/bigframes/core/compile/polars/operations/__init__.py similarity index 66% rename from tests/system/small/pandas/core/methods/__init__.py rename to bigframes/core/compile/polars/operations/__init__.py index 0a2669d7a2..26444dcb67 100644 --- a/tests/system/small/pandas/core/methods/__init__.py +++ b/bigframes/core/compile/polars/operations/__init__.py @@ -11,3 +11,11 @@ # 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. + +"""Operation implementations for the Polars LazyFrame compiler. + +This directory structure should reflect the same layout as the +`bigframes/operations` directory where the operations are defined. + +Prefer small groups of ops per file to keep file sizes manageable for text editors and LLMs. +""" diff --git a/bigframes/core/compile/polars/operations/generic_ops.py b/bigframes/core/compile/polars/operations/generic_ops.py new file mode 100644 index 0000000000..de0e987aa2 --- /dev/null +++ b/bigframes/core/compile/polars/operations/generic_ops.py @@ -0,0 +1,47 @@ +# Copyright 2025 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. + +""" +BigFrames -> Polars compilation for the operations in bigframes.operations.generic_ops. + +Please keep implementations in sequential order by op name. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import bigframes.core.compile.polars.compiler as polars_compiler +from bigframes.operations import generic_ops + +if TYPE_CHECKING: + import polars as pl + + +@polars_compiler.register_op(generic_ops.NotNullOp) +def notnull_op_impl( + compiler: polars_compiler.PolarsExpressionCompiler, + op: generic_ops.NotNullOp, # type: ignore + input: pl.Expr, +) -> pl.Expr: + return input.is_not_null() + + +@polars_compiler.register_op(generic_ops.IsNullOp) +def isnull_op_impl( + compiler: polars_compiler.PolarsExpressionCompiler, + op: generic_ops.IsNullOp, # type: ignore + input: pl.Expr, +) -> pl.Expr: + return input.is_null() diff --git a/noxfile.py b/noxfile.py index 2d0edfc1b0..7adf499a08 100644 --- a/noxfile.py +++ b/noxfile.py @@ -78,15 +78,20 @@ ] UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = [] UNIT_TEST_DEPENDENCIES: List[str] = [] -UNIT_TEST_EXTRAS: List[str] = ["tests", "anywidget"] +UNIT_TEST_EXTRAS: List[str] = ["tests"] UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = { - "3.12": ["tests", "polars", "scikit-learn", "anywidget"], + "3.10": ["tests", "scikit-learn", "anywidget"], + "3.11": ["tests", "polars", "scikit-learn", "anywidget"], + # Make sure we leave some versions without "extras" so we know those + # dependencies are actually optional. + "3.13": ["tests", "polars", "scikit-learn", "anywidget"], } +# 3.11 is used by colab. # 3.10 is needed for Windows tests as it is the only version installed in the # bigframes-windows container image. For more information, search # bigframes/windows-docker, internally. -SYSTEM_TEST_PYTHON_VERSIONS = ["3.9", "3.10", "3.12", "3.13"] +SYSTEM_TEST_PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.13"] SYSTEM_TEST_STANDARD_DEPENDENCIES = [ "jinja2", "mock", @@ -105,12 +110,13 @@ ] SYSTEM_TEST_LOCAL_DEPENDENCIES: List[str] = [] SYSTEM_TEST_DEPENDENCIES: List[str] = [] -SYSTEM_TEST_EXTRAS: List[str] = [] +SYSTEM_TEST_EXTRAS: List[str] = ["tests"] SYSTEM_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = { - "3.9": ["tests", "anywidget"], - "3.10": ["tests", "polars"], - "3.12": ["tests", "scikit-learn", "polars", "anywidget"], - "3.13": ["tests", "polars"], + # Make sure we leave some versions without "extras" so we know those + # dependencies are actually optional. + "3.10": ["tests", "scikit-learn", "anywidget"], + "3.11": ["tests", "scikit-learn", "polars", "anywidget"], + "3.13": ["tests", "polars", "anywidget"], } LOGGING_NAME_ENV_VAR = "BIGFRAMES_PERFORMANCE_LOG_NAME" @@ -120,8 +126,8 @@ # Sessions are executed in the order so putting the smaller sessions # ahead to fail fast at presubmit running. nox.options.sessions = [ - "system-3.9", - "system-3.12", + "system-3.9", # No extras. + "system-3.11", "cover", # TODO(b/401609005): remove "cleanup", diff --git a/setup.py b/setup.py index bc42cc4281..2aef514749 100644 --- a/setup.py +++ b/setup.py @@ -76,8 +76,8 @@ "google-cloud-bigtable >=2.24.0", "google-cloud-pubsub >=2.21.4", ], - # used for local engine, which is only needed for unit tests at present. - "polars": ["polars >= 1.7.0"], + # used for local engine + "polars": ["polars >= 1.21.0"], "scikit-learn": ["scikit-learn>=1.2.2"], # Packages required for basic development flow. "dev": [ diff --git a/testing/constraints-3.10.txt b/testing/constraints-3.10.txt index 12ad443aab..1695a4806b 100644 --- a/testing/constraints-3.10.txt +++ b/testing/constraints-3.10.txt @@ -1,4 +1,5 @@ -# Keep in sync with colab/containers/requirements.core.in image +# When we drop Python 3.9, +# please keep these in sync with the minimum versions in setup.py google-auth==2.27.0 ipykernel==5.5.6 ipython==7.34.0 @@ -15,4 +16,4 @@ matplotlib==3.7.1 psutil==5.9.5 seaborn==0.13.1 traitlets==5.7.1 -polars==1.7.0 +polars==1.21.0 diff --git a/testing/constraints-3.11.txt b/testing/constraints-3.11.txt index e69de29bb2..8fd20d453b 100644 --- a/testing/constraints-3.11.txt +++ b/testing/constraints-3.11.txt @@ -0,0 +1,621 @@ +# Keep in sync with %pip freeze in colab. +# Note: These are just constraints, so it's ok to have extra packages we +# aren't installing, except in the version that gets used for prerelease +# tests. +absl-py==1.4.0 +accelerate==1.9.0 +aiofiles==24.1.0 +aiohappyeyeballs==2.6.1 +aiohttp==3.12.15 +aiosignal==1.4.0 +alabaster==1.0.0 +albucore==0.0.24 +albumentations==2.0.8 +ale-py==0.11.2 +altair==5.5.0 +annotated-types==0.7.0 +antlr4-python3-runtime==4.9.3 +anyio==4.10.0 +anywidget==0.9.18 +argon2-cffi==25.1.0 +argon2-cffi-bindings==25.1.0 +array_record==0.7.2 +arviz==0.22.0 +astropy==7.1.0 +astropy-iers-data==0.2025.8.4.0.42.59 +astunparse==1.6.3 +atpublic==5.1 +attrs==25.3.0 +audioread==3.0.1 +autograd==1.8.0 +babel==2.17.0 +backcall==0.2.0 +backports.tarfile==1.2.0 +beautifulsoup4==4.13.4 +betterproto==2.0.0b6 +bigquery-magics==0.10.2 +bleach==6.2.0 +blinker==1.9.0 +blis==1.3.0 +blobfile==3.0.0 +blosc2==3.6.1 +bokeh==3.7.3 +Bottleneck==1.4.2 +bqplot==0.12.45 +branca==0.8.1 +Brotli==1.1.0 +build==1.3.0 +CacheControl==0.14.3 +cachetools==5.5.2 +catalogue==2.0.10 +certifi==2025.8.3 +cffi==1.17.1 +chardet==5.2.0 +charset-normalizer==3.4.2 +chex==0.1.90 +clarabel==0.11.1 +click==8.2.1 +cloudpathlib==0.21.1 +cloudpickle==3.1.1 +cmake==3.31.6 +cmdstanpy==1.2.5 +colorcet==3.1.0 +colorlover==0.3.0 +colour==0.1.5 +community==1.0.0b1 +confection==0.1.5 +cons==0.4.7 +contourpy==1.3.3 +cramjam==2.11.0 +cryptography==43.0.3 +cuda-python==12.6.2.post1 +cudf-polars-cu12==25.6.0 +cufflinks==0.17.3 +cuml-cu12==25.6.0 +cupy-cuda12x==13.3.0 +curl_cffi==0.12.0 +cuvs-cu12==25.6.1 +cvxopt==1.3.2 +cvxpy==1.6.7 +cycler==0.12.1 +cyipopt==1.5.0 +cymem==2.0.11 +Cython==3.0.12 +dask==2025.5.0 +dask-cuda==25.6.0 +dask-cudf-cu12==25.6.0 +dataproc-spark-connect==0.8.3 +datasets==4.0.0 +db-dtypes==1.4.3 +dbus-python==1.2.18 +debugpy==1.8.15 +decorator==4.4.2 +defusedxml==0.7.1 +diffusers==0.34.0 +dill==0.3.8 +distributed==2025.5.0 +distributed-ucxx-cu12==0.44.0 +distro==1.9.0 +dlib==19.24.6 +dm-tree==0.1.9 +docstring_parser==0.17.0 +docutils==0.21.2 +dopamine_rl==4.1.2 +duckdb==1.3.2 +earthengine-api==1.5.24 +easydict==1.13 +editdistance==0.8.1 +eerepr==0.1.2 +einops==0.8.1 +entrypoints==0.4 +et_xmlfile==2.0.0 +etils==1.13.0 +etuples==0.3.10 +Farama-Notifications==0.0.4 +fastai==2.7.19 +fastapi==0.116.1 +fastcore==1.7.29 +fastdownload==0.0.7 +fastjsonschema==2.21.1 +fastprogress==1.0.3 +fastrlock==0.8.3 +ffmpy==0.6.1 +filelock==3.18.0 +firebase-admin==6.9.0 +Flask==3.1.1 +flatbuffers==25.2.10 +flax==0.10.6 +folium==0.20.0 +fonttools==4.59.0 +frozendict==2.4.6 +frozenlist==1.7.0 +fsspec==2025.3.0 +future==1.0.0 +gast==0.6.0 +gcsfs==2025.3.0 +GDAL==3.8.4 +gdown==5.2.0 +geemap==0.35.3 +geocoder==1.38.1 +geographiclib==2.0 +geopandas==1.1.1 +geopy==2.4.1 +gin-config==0.5.0 +gitdb==4.0.12 +GitPython==3.1.45 +glob2==0.7 +google==2.0.3 +google-ai-generativelanguage==0.6.15 +google-api-core==2.25.1 +google-api-python-client==2.177.0 +google-auth==2.38.0 +google-auth-httplib2==0.2.0 +google-auth-oauthlib==1.2.2 +google-cloud-aiplatform==1.106.0 +google-cloud-bigquery==3.35.1 +google-cloud-bigquery-connection==1.18.3 +google-cloud-bigquery-storage==2.32.0 +google-cloud-core==2.4.3 +google-cloud-dataproc==5.21.0 +google-cloud-datastore==2.21.0 +google-cloud-firestore==2.21.0 +google-cloud-functions==1.20.4 +google-cloud-language==2.17.2 +google-cloud-resource-manager==1.14.2 +google-cloud-spanner==3.56.0 +google-cloud-storage==2.19.0 +google-cloud-translate==3.21.1 +google-crc32c==1.7.1 +google-genai==1.28.0 +google-generativeai==0.8.5 +google-pasta==0.2.0 +google-resumable-media==2.7.2 +googleapis-common-protos==1.70.0 +googledrivedownloader==1.1.0 +gradio==5.39.0 +gradio_client==1.11.0 +graphviz==0.21 +greenlet==3.2.3 +groovy==0.1.2 +grpc-google-iam-v1==0.14.2 +grpc-interceptor==0.15.4 +grpcio==1.74.0 +grpcio-status==1.71.2 +grpclib==0.4.8 +gspread==6.2.1 +gspread-dataframe==4.0.0 +gym==0.25.2 +gym-notices==0.1.0 +gymnasium==1.2.0 +h11==0.16.0 +h2==4.2.0 +h5netcdf==1.6.3 +h5py==3.14.0 +hdbscan==0.8.40 +hf-xet==1.1.5 +hf_transfer==0.1.9 +highspy==1.11.0 +holidays==0.78 +holoviews==1.21.0 +hpack==4.1.0 +html5lib==1.1 +httpcore==1.0.9 +httpimport==1.4.1 +httplib2==0.22.0 +httpx==0.28.1 +huggingface-hub==0.34.3 +humanize==4.12.3 +hyperframe==6.1.0 +hyperopt==0.2.7 +ibis-framework==9.5.0 +idna==3.10 +imageio==2.37.0 +imageio-ffmpeg==0.6.0 +imagesize==1.4.1 +imbalanced-learn==0.13.0 +immutabledict==4.2.1 +importlib_metadata==8.7.0 +importlib_resources==6.5.2 +imutils==0.5.4 +inflect==7.5.0 +iniconfig==2.1.0 +intel-cmplr-lib-ur==2025.2.0 +intel-openmp==2025.2.0 +ipyevents==2.0.2 +ipyfilechooser==0.6.0 +ipykernel==6.17.1 +ipyleaflet==0.20.0 +ipyparallel==8.8.0 +ipython==7.34.0 +ipython-genutils==0.2.0 +ipython-sql==0.5.0 +ipytree==0.2.2 +ipywidgets==7.7.1 +itsdangerous==2.2.0 +jaraco.classes==3.4.0 +jaraco.context==6.0.1 +jaraco.functools==4.2.1 +jax==0.5.3 +jax-cuda12-pjrt==0.5.3 +jax-cuda12-plugin==0.5.3 +jaxlib==0.5.3 +jeepney==0.9.0 +jieba==0.42.1 +Jinja2==3.1.6 +jiter==0.10.0 +joblib==1.5.1 +jsonpatch==1.33 +jsonpickle==4.1.1 +jsonpointer==3.0.0 +jsonschema==4.25.0 +jsonschema-specifications==2025.4.1 +jupyter-client==6.1.12 +jupyter-console==6.1.0 +jupyter-leaflet==0.20.0 +jupyter-server==1.16.0 +jupyter_core==5.8.1 +jupyterlab_pygments==0.3.0 +jupyterlab_widgets==3.0.15 +jupytext==1.17.2 +kaggle==1.7.4.5 +kagglehub==0.3.12 +keras==3.10.0 +keras-hub==0.21.1 +keras-nlp==0.21.1 +keyring==25.6.0 +keyrings.google-artifactregistry-auth==1.1.2 +kiwisolver==1.4.8 +langchain==0.3.27 +langchain-core==0.3.72 +langchain-text-splitters==0.3.9 +langcodes==3.5.0 +langsmith==0.4.10 +language_data==1.3.0 +launchpadlib==1.10.16 +lazr.restfulclient==0.14.4 +lazr.uri==1.0.6 +lazy_loader==0.4 +libclang==18.1.1 +libcugraph-cu12==25.6.0 +libcuml-cu12==25.6.0 +libcuvs-cu12==25.6.1 +libkvikio-cu12==25.6.0 +libpysal==4.13.0 +libraft-cu12==25.6.0 +librmm-cu12==25.6.0 +librosa==0.11.0 +libucx-cu12==1.18.1 +libucxx-cu12==0.44.0 +linkify-it-py==2.0.3 +llvmlite==0.43.0 +locket==1.0.0 +logical-unification==0.4.6 +lxml==5.4.0 +Mako==1.1.3 +marisa-trie==1.2.1 +Markdown==3.8.2 +markdown-it-py==3.0.0 +MarkupSafe==3.0.2 +matplotlib==3.10.0 +matplotlib-inline==0.1.7 +matplotlib-venn==1.1.2 +mdit-py-plugins==0.4.2 +mdurl==0.1.2 +miniKanren==1.0.5 +missingno==0.5.2 +mistune==3.1.3 +mizani==0.13.5 +mkl==2025.2.0 +ml_dtypes==0.5.3 +mlxtend==0.23.4 +more-itertools==10.7.0 +moviepy==1.0.3 +mpmath==1.3.0 +msgpack==1.1.1 +multidict==6.6.3 +multipledispatch==1.0.0 +multiprocess==0.70.16 +multitasking==0.0.12 +murmurhash==1.0.13 +music21==9.3.0 +namex==0.1.0 +narwhals==2.0.1 +natsort==8.4.0 +nbclassic==1.3.1 +nbclient==0.10.2 +nbconvert==7.16.6 +nbformat==5.10.4 +ndindex==1.10.0 +nest-asyncio==1.6.0 +networkx==3.5 +nibabel==5.3.2 +nltk==3.9.1 +notebook==6.5.7 +notebook_shim==0.2.4 +numba==0.60.0 +numba-cuda==0.11.0 +numexpr==2.11.0 +numpy==2.0.2 +nvidia-cublas-cu12==12.5.3.2 +nvidia-cuda-cupti-cu12==12.5.82 +nvidia-cuda-nvcc-cu12==12.5.82 +nvidia-cuda-nvrtc-cu12==12.5.82 +nvidia-cuda-runtime-cu12==12.5.82 +nvidia-cudnn-cu12==9.3.0.75 +nvidia-cufft-cu12==11.2.3.61 +nvidia-curand-cu12==10.3.6.82 +nvidia-cusolver-cu12==11.6.3.83 +nvidia-cusparse-cu12==12.5.1.3 +nvidia-cusparselt-cu12==0.6.2 +nvidia-ml-py==12.575.51 +nvidia-nccl-cu12==2.23.4 +nvidia-nvjitlink-cu12==12.5.82 +nvidia-nvtx-cu12==12.4.127 +nvtx==0.2.13 +oauth2client==4.1.3 +oauthlib==3.3.1 +omegaconf==2.3.0 +openai==1.98.0 +opencv-contrib-python==4.12.0.88 +opencv-python==4.12.0.88 +opencv-python-headless==4.12.0.88 +openpyxl==3.1.5 +opt_einsum==3.4.0 +optax==0.2.5 +optree==0.17.0 +orbax-checkpoint==0.11.20 +orjson==3.11.1 +osqp==1.0.4 +packaging==25.0 +pandas==2.2.2 +pandas-datareader==0.10.0 +pandas-gbq==0.29.2 +pandas-stubs==2.2.2.240909 +pandocfilters==1.5.1 +panel==1.7.5 +param==2.2.1 +parso==0.8.4 +parsy==2.1 +partd==1.4.2 +patsy==1.0.1 +peewee==3.18.2 +peft==0.17.0 +pexpect==4.9.0 +pickleshare==0.7.5 +pillow==11.3.0 +platformdirs==4.3.8 +plotly==5.24.1 +plotnine==0.14.5 +pluggy==1.6.0 +ply==3.11 +polars==1.25.2 +pooch==1.8.2 +portpicker==1.5.2 +preshed==3.0.10 +prettytable==3.16.0 +proglog==0.1.12 +progressbar2==4.5.0 +prometheus_client==0.22.1 +promise==2.3 +prompt_toolkit==3.0.51 +propcache==0.3.2 +prophet==1.1.7 +proto-plus==1.26.1 +protobuf==5.29.5 +psutil==5.9.5 +psycopg2==2.9.10 +psygnal==0.14.0 +ptyprocess==0.7.0 +py-cpuinfo==9.0.0 +py4j==0.10.9.7 +pyarrow==18.1.0 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +pycairo==1.28.0 +pycocotools==2.0.10 +pycparser==2.22 +pycryptodomex==3.23.0 +pydantic==2.11.7 +pydantic_core==2.33.2 +pydata-google-auth==1.9.1 +pydot==3.0.4 +pydotplus==2.0.2 +PyDrive2==1.21.3 +pydub==0.25.1 +pyerfa==2.0.1.5 +pygame==2.6.1 +pygit2==1.18.1 +Pygments==2.19.2 +PyGObject==3.42.0 +PyJWT==2.10.1 +pylibcugraph-cu12==25.6.0 +pylibraft-cu12==25.6.0 +pymc==5.25.1 +pynndescent==0.5.13 +pynvjitlink-cu12==0.7.0 +pynvml==12.0.0 +pyogrio==0.11.1 +pyomo==6.9.2 +PyOpenGL==3.1.9 +pyOpenSSL==24.2.1 +pyparsing==3.2.3 +pyperclip==1.9.0 +pyproj==3.7.1 +pyproject_hooks==1.2.0 +pyshp==2.3.1 +PySocks==1.7.1 +pyspark==3.5.1 +pytensor==2.31.7 +python-apt==0.0.0 +python-box==7.3.2 +python-dateutil==2.9.0.post0 +python-louvain==0.16 +python-multipart==0.0.20 +python-slugify==8.0.4 +python-snappy==0.7.3 +python-utils==3.9.1 +pytz==2025.2 +pyviz_comms==3.0.6 +PyWavelets==1.9.0 +PyYAML==6.0.2 +pyzmq==26.2.1 +raft-dask-cu12==25.6.0 +rapids-dask-dependency==25.6.0 +rapids-logger==0.1.1 +ratelim==0.1.6 +referencing==0.36.2 +regex==2024.11.6 +requests==2.32.3 +requests-oauthlib==2.0.0 +requests-toolbelt==1.0.0 +requirements-parser==0.9.0 +rich==13.9.4 +rmm-cu12==25.6.0 +roman-numerals-py==3.1.0 +rpds-py==0.26.0 +rpy2==3.5.17 +rsa==4.9.1 +ruff==0.12.7 +safehttpx==0.1.6 +safetensors==0.5.3 +scikit-image==0.25.2 +scikit-learn==1.6.1 +scipy==1.16.1 +scooby==0.10.1 +scs==3.2.7.post2 +seaborn==0.13.2 +SecretStorage==3.3.3 +semantic-version==2.10.0 +Send2Trash==1.8.3 +sentence-transformers==4.1.0 +sentencepiece==0.2.0 +sentry-sdk==2.34.1 +shap==0.48.0 +shapely==2.1.1 +shellingham==1.5.4 +simple-parsing==0.1.7 +simplejson==3.20.1 +simsimd==6.5.0 +six==1.17.0 +sklearn-compat==0.1.3 +sklearn-pandas==2.2.0 +slicer==0.0.8 +smart_open==7.3.0.post1 +smmap==5.0.2 +sniffio==1.3.1 +snowballstemmer==3.0.1 +sortedcontainers==2.4.0 +soundfile==0.13.1 +soupsieve==2.7 +soxr==0.5.0.post1 +spacy==3.8.7 +spacy-legacy==3.0.12 +spacy-loggers==1.0.5 +spanner-graph-notebook==1.1.7 +Sphinx==8.2.3 +sphinxcontrib-applehelp==2.0.0 +sphinxcontrib-devhelp==2.0.0 +sphinxcontrib-htmlhelp==2.1.0 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==2.0.0 +sphinxcontrib-serializinghtml==2.0.0 +SQLAlchemy==2.0.42 +sqlglot==25.20.2 +sqlparse==0.5.3 +srsly==2.5.1 +stanio==0.5.1 +starlette==0.47.2 +statsmodels==0.14.5 +stringzilla==3.12.5 +stumpy==1.13.0 +sympy==1.13.1 +tables==3.10.2 +tabulate==0.9.0 +tbb==2022.2.0 +tblib==3.1.0 +tcmlib==1.4.0 +tenacity==8.5.0 +tensorboard==2.19.0 +tensorboard-data-server==0.7.2 +tensorflow==2.19.0 +tensorflow-datasets==4.9.9 +tensorflow-hub==0.16.1 +tensorflow-io-gcs-filesystem==0.37.1 +tensorflow-metadata==1.17.2 +tensorflow-probability==0.25.0 +tensorflow-text==2.19.0 +tensorflow_decision_forests==1.12.0 +tensorstore==0.1.76 +termcolor==3.1.0 +terminado==0.18.1 +text-unidecode==1.3 +textblob==0.19.0 +tf-slim==1.1.0 +tf_keras==2.19.0 +thinc==8.3.6 +threadpoolctl==3.6.0 +tifffile==2025.6.11 +tiktoken==0.9.0 +timm==1.0.19 +tinycss2==1.4.0 +tokenizers==0.21.4 +toml==0.10.2 +tomlkit==0.13.3 +toolz==0.12.1 +torchao==0.10.0 +torchdata==0.11.0 +torchsummary==1.5.1 +torchtune==0.6.1 +tornado==6.4.2 +tqdm==4.67.1 +traitlets==5.7.1 +traittypes==0.2.1 +transformers==4.54.1 +treelite==4.4.1 +treescope==0.1.9 +triton==3.2.0 +tsfresh==0.21.0 +tweepy==4.16.0 +typeguard==4.4.4 +typer==0.16.0 +types-pytz==2025.2.0.20250516 +types-setuptools==80.9.0.20250801 +typing-inspection==0.4.1 +typing_extensions==4.14.1 +tzdata==2025.2 +tzlocal==5.3.1 +uc-micro-py==1.0.3 +ucx-py-cu12==0.44.0 +ucxx-cu12==0.44.0 +umap-learn==0.5.9.post2 +umf==0.11.0 +uritemplate==4.2.0 +urllib3==2.5.0 +uvicorn==0.35.0 +vega-datasets==0.9.0 +wadllib==1.3.6 +wandb==0.21.0 +wasabi==1.1.3 +wcwidth==0.2.13 +weasel==0.4.1 +webcolors==24.11.1 +webencodings==0.5.1 +websocket-client==1.8.0 +websockets==15.0.1 +Werkzeug==3.1.3 +widgetsnbextension==3.6.10 +wordcloud==1.9.4 +wrapt==1.17.2 +wurlitzer==3.1.1 +xarray==2025.7.1 +xarray-einstats==0.9.1 +xgboost==3.0.3 +xlrd==2.0.2 +xxhash==3.5.0 +xyzservices==2025.4.0 +yarl==1.20.1 +ydf==0.13.0 +yellowbrick==1.5 +yfinance==0.2.65 +zict==3.0.0 +zipp==3.23.0 diff --git a/tests/system/small/pandas/core/methods/test_describe.py b/tests/system/small/pandas/test_describe.py similarity index 100% rename from tests/system/small/pandas/core/methods/test_describe.py rename to tests/system/small/pandas/test_describe.py diff --git a/tests/system/small/pandas/io/api/test_read_gbq_colab.py b/tests/system/small/pandas/test_read_gbq_colab.py similarity index 100% rename from tests/system/small/pandas/io/api/test_read_gbq_colab.py rename to tests/system/small/pandas/test_read_gbq_colab.py diff --git a/tests/system/small/test_polars_execution.py b/tests/system/small/test_polars_execution.py index 1568a76ec9..916780b1ce 100644 --- a/tests/system/small/test_polars_execution.py +++ b/tests/system/small/test_polars_execution.py @@ -16,7 +16,7 @@ import bigframes from bigframes.testing.utils import assert_pandas_df_equal -polars = pytest.importorskip("polars", reason="polars is required for this test") +polars = pytest.importorskip("polars") @pytest.fixture(scope="module")