Skip to content

Commit 756bd5f

Browse files
committed
Merge remote-tracking branch 'origin/main' into tswast-bbq
2 parents b8c42bb + ee74e31 commit 756bd5f

52 files changed

Lines changed: 1674 additions & 502 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

mypy.ini

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
[mypy]
2+
namespace_packages = True
3+
ignore_missing_imports = False
4+
5+
# Helps mypy navigate the "google" namespace more reliably in 3.10+
6+
explicit_package_bases = True
7+
8+
# Performance: reuse results from previous runs to speed up "nox"
9+
incremental = True
10+
11+
exclude = (?x)(
12+
(^|/)third_party/
13+
| (^|/)tests/unit/resources/
14+
| (^|/)tests/unit/gapic/
15+
)
16+
17+
18+
# ==============================================================================
19+
# GLOBAL THIRD-PARTY & SHARED LIBRARY IGNORES
20+
# ==============================================================================
21+
22+
[mypy-anywidget]
23+
ignore_missing_imports = True
24+
25+
[mypy-cloudpickle.*]
26+
ignore_missing_imports = True
27+
28+
[mypy-flask]
29+
ignore_missing_imports = True
30+
31+
[mypy-google.auth.*]
32+
ignore_missing_imports = True
33+
34+
[mypy-google.cloud.bigtable]
35+
ignore_missing_imports = True
36+
37+
[mypy-google.cloud.pubsub]
38+
ignore_missing_imports = True
39+
40+
[mypy-google.colab]
41+
ignore_missing_imports = True
42+
43+
[mypy-google.iam.*]
44+
ignore_missing_imports = True
45+
46+
[mypy-google.longrunning.*]
47+
ignore_missing_imports = True
48+
49+
[mypy-google.oauth2.*]
50+
ignore_missing_imports = True
51+
52+
[mypy-google.protobuf.*]
53+
ignore_missing_imports = True
54+
55+
[mypy-google.rpc.*]
56+
ignore_missing_imports = True
57+
58+
[mypy-google.type.*]
59+
ignore_missing_imports = True
60+
61+
[mypy-grpc.*]
62+
ignore_missing_imports = True
63+
64+
[mypy-ibis.*]
65+
ignore_missing_imports = True
66+
67+
[mypy-ipywidgets]
68+
ignore_missing_imports = True
69+
70+
[mypy-proto.*]
71+
ignore_missing_imports = True
72+
73+
[mypy-pyarrow.*]
74+
ignore_missing_imports = True
75+
76+
[mypy-pydata_google_auth]
77+
ignore_missing_imports = True
78+
79+
[mypy-pytest]
80+
ignore_missing_imports = True
81+
82+
[mypy-pytz]
83+
ignore_missing_imports = True
84+
85+
86+
# ==============================================================================
87+
# PACKAGE-SPECIFIC OVERRIDES & EXCEPTIONS
88+
# ==============================================================================
89+
90+
# --- google-cloud-bigtable ---
91+
[mypy-google.cloud.bigtable.*]
92+
ignore_errors = True
93+
94+
[mypy-google.cloud.bigtable.data.*]
95+
check_untyped_defs = True
96+
warn_unreachable = True
97+
disallow_any_generics = True
98+
ignore_errors = False
99+
100+

packages/bigframes/bigframes/_config/experiment_options.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self):
2828
self._semantic_operators: bool = False
2929
self._ai_operators: bool = False
3030
self._sql_compiler: Literal["legacy", "stable", "experimental"] = "stable"
31+
self._enable_python_transpiler: bool = False
3132

3233
@property
3334
def semantic_operators(self) -> bool:
@@ -166,3 +167,17 @@ def blob_display_height(self, value: Optional[int]):
166167
warnings.warn(msg, category=bfe.ApiDeprecationWarning)
167168

168169
bigframes.options.display.blob_display_height = value
170+
171+
@property
172+
def enable_python_transpiler(self) -> bool:
173+
return self._enable_python_transpiler
174+
175+
@enable_python_transpiler.setter
176+
def enable_python_transpiler(self, value: bool):
177+
if value:
178+
msg = bfe.format_message(
179+
"Python transpiler is an unstable, experimental feature, and not yet fully "
180+
"validated, use at your own risk."
181+
)
182+
warnings.warn(msg, category=bfe.PythonTranspilerPreviewWarning)
183+
self._enable_python_transpiler = value

packages/bigframes/bigframes/_tools/docs.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ def decorator(target_class):
3434
except AttributeError:
3535
pass
3636

37+
underlying = None
38+
if isinstance(target_item, property):
39+
underlying = target_item.fget
40+
elif hasattr(target_item, "__func__"):
41+
underlying = target_item.__func__
42+
elif hasattr(target_item, "func"):
43+
underlying = getattr(target_item, "func", None)
44+
45+
if underlying is not None:
46+
try:
47+
underlying.__doc__ = source_item.__doc__
48+
except AttributeError:
49+
pass
50+
3751
return target_class
3852

3953
return decorator

packages/bigframes/bigframes/core/block_transforms.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,58 @@
1414
from __future__ import annotations
1515

1616
import functools
17+
import inspect
1718
import typing
18-
from typing import Optional, Sequence
19+
from typing import Callable, Hashable, Optional, Sequence
1920

2021
import bigframes_vendored.constants as constants
2122
import pandas as pd
2223

2324
import bigframes.constants
2425
import bigframes.core as core
2526
import bigframes.core.blocks as blocks
27+
import bigframes.core.bytecode as bytecode
2628
import bigframes.core.expression as ex
2729
import bigframes.core.ordering as ordering
2830
import bigframes.core.window_spec as windows
2931
import bigframes.dtypes as dtypes
3032
import bigframes.operations as ops
3133
import bigframes.operations.aggregations as agg_ops
32-
from bigframes.core import agg_expressions
34+
from bigframes.core import agg_expressions, py_expressions
35+
36+
37+
def apply_to_block_rows(
38+
func: Callable, block: blocks.Block, *args, **kwargs
39+
) -> blocks.Block:
40+
"""
41+
Apply the given function to each row of the block.
42+
43+
The function is applied to each row of the block, and the result is returned
44+
as a new block with the same index.
45+
"""
46+
expr = bytecode._compile_bytecode_to_py_expr(func)
47+
sig = inspect.signature(func)
48+
49+
bindings: dict[Hashable, ex.Expression] = {}
50+
51+
bound_args = sig.bind(*(None, *args), **kwargs)
52+
bound_args.apply_defaults()
53+
bound_params = bound_args.arguments
54+
for name, value in bound_params.items():
55+
bindings[name] = ex.const(value)
56+
57+
expr = py_expressions.resolve_py_exprs(
58+
expr,
59+
series_arg=next(iter(sig.parameters.keys())),
60+
series_attrs={
61+
label: col_id
62+
for label in block.column_labels
63+
if (col_id := block.resolve_label_exact(label)) is not None
64+
},
65+
)
66+
expr = expr.bind_variables(bindings)
67+
68+
return block.project_exprs([expr], labels=[None], drop=True)
3369

3470

3571
def equals(block1: blocks.Block, block2: blocks.Block) -> bool:

packages/bigframes/bigframes/core/bytecode.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,12 @@ def _compile_bytecode_to_py_expr(func: Callable) -> expression.Expression:
249249
raise ValueError("No return value found")
250250

251251

252-
def dis_to_expr(func: Callable, unpack_mode: bool = False) -> expression.Expression:
252+
def py_to_expression(func: Callable) -> expression.Expression:
253253
"""
254254
Try to convert a python function to a BigQuery expression.
255255
256-
Unpack mode is whether SQL columns are addressed as attributes of a single
257-
python argument (e.g. row.col1), or as separate arguments (e.g. col1).
258-
259256
This is "best effort" - if the function contains operations that cannot
260257
be converted to BigQuery expressions, it will raise an Exception.
261258
"""
262259
py_expr = _compile_bytecode_to_py_expr(func)
263-
return py_exprs.resolve_py_exprs(py_expr, unpack_mode=unpack_mode)
260+
return py_exprs.resolve_py_exprs(py_expr)

packages/bigframes/bigframes/core/py_expressions.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import dataclasses
1818
import itertools
1919
from types import ModuleType
20-
from typing import Callable, Hashable, Mapping, Tuple
20+
from typing import Callable, Hashable, Mapping, Optional, Tuple
2121

2222
import bigframes.operations.python_op_maps as python_op_maps
2323
from bigframes import dtypes
@@ -27,6 +27,7 @@
2727
OpExpression,
2828
UnboundVariableExpression,
2929
const,
30+
deref,
3031
)
3132
from bigframes.operations import NUMPY_TO_BINOP, NUMPY_TO_OP, generic_ops, numeric_ops
3233

@@ -310,7 +311,11 @@ def bind_refs(
310311

311312

312313
# TODO: Mode that resolves free variable attrs as columns
313-
def resolve_py_exprs(expression: Expression, unpack_mode: bool = False) -> Expression:
314+
def resolve_py_exprs(
315+
expression: Expression,
316+
series_arg: Optional[str] = None,
317+
series_attrs: Mapping[Hashable, str] | None = None,
318+
) -> Expression:
314319
"""Replace all PyObject, attribute, call expressions. Bottom-up."""
315320

316321
def resolve_expr_if_call(expression: Expression) -> Expression:
@@ -325,10 +330,15 @@ def resolve_attrs(expression: Expression) -> Expression:
325330
if isinstance(expression.input, Module):
326331
# resolves things like Math.pi
327332
return PyObject(getattr(expression.input.module, expression.attr))
328-
if not unpack_mode and isinstance(
329-
expression.input, UnboundVariableExpression
333+
# TODO: Resolve some series methods
334+
if (
335+
series_arg is not None
336+
and series_attrs is not None
337+
and isinstance(expression.input, UnboundVariableExpression)
338+
and expression.input.id == series_arg
339+
and expression.attr in series_attrs
330340
):
331-
return UnboundVariableExpression(expression.attr)
341+
return deref(series_attrs[expression.attr])
332342
return expression
333343

334344
def resolve_pyobjs(expression: Expression) -> Expression:

packages/bigframes/bigframes/dataframe.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4716,13 +4716,17 @@ def _prepare_export(
47164716
return array_value, id_overrides
47174717

47184718
def map(self, func, na_action: Optional[str] = None) -> DataFrame:
4719-
if not isinstance(func, bigframes.functions.Udf):
4719+
from bigframes._config import options
4720+
4721+
if not isinstance(func, bigframes.functions.Udf) and not (
4722+
options.experiments.enable_python_transpiler and callable(func)
4723+
):
47204724
raise TypeError("the first argument must be callable")
47214725

47224726
if na_action not in {None, "ignore"}:
47234727
raise ValueError(f"na_action={na_action} not supported")
47244728

4725-
expr = ops.func_to_op(func).as_expr(ex.free_var("input"))
4729+
expr = ops.func_to_expr(func).apply(ex.free_var("input"))
47264730
if na_action == "ignore":
47274731
# True case, predicate, False case
47284732
expr = ops.where_op.as_expr(
@@ -4742,11 +4746,25 @@ def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs):
47424746
)
47434747
warnings.warn(msg, category=bfe.FunctionAxisOnePreviewWarning)
47444748

4745-
if not isinstance(func, bigframes.functions.Udf):
4749+
from bigframes._config import options
4750+
4751+
if not isinstance(func, bigframes.functions.Udf) and not (
4752+
options.experiments.enable_python_transpiler and callable(func)
4753+
):
47464754
raise ValueError(
47474755
"For axis=1 a BigFrames BigQuery function must be used."
47484756
)
47494757

4758+
if (
4759+
not isinstance(func, bigframes.functions.Udf)
4760+
and options.experiments.enable_python_transpiler
4761+
and callable(func)
4762+
):
4763+
result_block = block_ops.apply_to_block_rows(
4764+
func, self._block, *args, **kwargs
4765+
)
4766+
return bigframes.series.Series(result_block)
4767+
47504768
if func.udf_def.signature.is_row_processor:
47514769
# Early check whether the dataframe dtypes are currently supported
47524770
# in the bigquery function
@@ -4800,8 +4818,14 @@ def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs):
48004818
)
48014819

48024820
# Apply the function
4821+
expr = ops.func_to_expr(func).expr
4822+
if not (
4823+
isinstance(expr, ex.OpExpression)
4824+
and isinstance(expr.op, ops.NaryOp)
4825+
):
4826+
raise TypeError(f"Expected OpExpression with NaryOp, got {expr}")
48034827
result_series = rows_as_json_series._apply_nary_op(
4804-
ops.func_to_op(func),
4828+
expr.op,
48054829
list(args),
48064830
)
48074831

@@ -4861,8 +4885,8 @@ def apply(self, func, *, axis=0, args: typing.Tuple = (), **kwargs):
48614885

48624886
series_list = [self[col] for col in self.columns]
48634887
op_list = series_list[1:] + list(args)
4864-
result_series = series_list[0]._apply_nary_op(
4865-
ops.func_to_op(func), op_list
4888+
result_series = series_list[0]._apply_callable_expr(
4889+
ops.func_to_expr(func), op_list
48664890
)
48674891
result_series.name = None
48684892

packages/bigframes/bigframes/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ class MaximumResultRowsExceeded(RuntimeError):
7575
"""Maximum number of rows in the result was exceeded."""
7676

7777

78+
class TranspilationError(RuntimeError):
79+
"""Failed to transpile a Python function to BigFrames Expression."""
80+
81+
7882
class TimeTravelDisabledWarning(Warning):
7983
"""A query was reattempted without time travel."""
8084

@@ -126,6 +130,10 @@ class FunctionPackageVersionWarning(PreviewWarning):
126130
"""
127131

128132

133+
class PythonTranspilerPreviewWarning(PreviewWarning):
134+
"""Python Transpiler is a preview feature."""
135+
136+
129137
def format_message(message: str, fill: bool = True):
130138
"""[Private] Formats a warning message.
131139

0 commit comments

Comments
 (0)