Skip to content

Commit 5f7b8b1

Browse files
authored
feat: support context manager for bigframes session (#1107)
* feat: support context manager for bigframes session * add table cleanup tests for session context manager * rename ctor, split large test
1 parent 5528b4d commit 5f7b8b1

File tree

4 files changed

+208
-5
lines changed

4 files changed

+208
-5
lines changed

bigframes/session/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,23 @@ def __init__(
275275
)
276276

277277
def __del__(self):
278-
"""Automatic cleanup of internal resources"""
278+
"""Automatic cleanup of internal resources."""
279+
self.close()
280+
281+
def __enter__(self):
282+
"""Enter the runtime context of the Session object.
283+
284+
See [With Statement Context Managers](https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers)
285+
for more details.
286+
"""
287+
return self
288+
289+
def __exit__(self, *_):
290+
"""Exit the runtime context of the Session object.
291+
292+
See [With Statement Context Managers](https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers)
293+
for more details.
294+
"""
279295
self.close()
280296

281297
@property

tests/system/large/test_remote_function.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2245,3 +2245,155 @@ def test_remote_function_ingress_settings_unsupported(session):
22452245
@session.remote_function(reuse=False, cloud_function_ingress_settings="unknown")
22462246
def square(x: int) -> int:
22472247
return x * x
2248+
2249+
2250+
@pytest.mark.parametrize(
2251+
("session_creator"),
2252+
[
2253+
pytest.param(bigframes.Session, id="session-constructor"),
2254+
pytest.param(bigframes.connect, id="connect-method"),
2255+
],
2256+
)
2257+
@pytest.mark.flaky(retries=2, delay=120)
2258+
def test_remote_function_w_context_manager_unnamed(
2259+
scalars_dfs, dataset_id, bq_cf_connection, session_creator
2260+
):
2261+
def add_one(x: int) -> int:
2262+
return x + 1
2263+
2264+
scalars_df, scalars_pandas_df = scalars_dfs
2265+
pd_result = scalars_pandas_df["int64_too"].apply(add_one)
2266+
2267+
temporary_bigquery_remote_function = None
2268+
temporary_cloud_run_function = None
2269+
2270+
try:
2271+
with session_creator() as session:
2272+
# create a temporary remote function
2273+
add_one_remote_temp = session.remote_function(
2274+
dataset=dataset_id,
2275+
bigquery_connection=bq_cf_connection,
2276+
reuse=False,
2277+
)(add_one)
2278+
2279+
temporary_bigquery_remote_function = (
2280+
add_one_remote_temp.bigframes_remote_function
2281+
)
2282+
assert temporary_bigquery_remote_function is not None
2283+
assert (
2284+
session.bqclient.get_routine(temporary_bigquery_remote_function)
2285+
is not None
2286+
)
2287+
2288+
temporary_cloud_run_function = add_one_remote_temp.bigframes_cloud_function
2289+
assert temporary_cloud_run_function is not None
2290+
assert (
2291+
session.cloudfunctionsclient.get_function(
2292+
name=temporary_cloud_run_function
2293+
)
2294+
is not None
2295+
)
2296+
2297+
bf_result = scalars_df["int64_too"].apply(add_one_remote_temp).to_pandas()
2298+
pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False)
2299+
2300+
# outside the with statement context manager the temporary BQ remote
2301+
# function and the underlying cloud run function should have been
2302+
# cleaned up
2303+
assert temporary_bigquery_remote_function is not None
2304+
with pytest.raises(google.api_core.exceptions.NotFound):
2305+
session.bqclient.get_routine(temporary_bigquery_remote_function)
2306+
# the deletion of cloud function happens in a non-blocking way, ensure that
2307+
# it either exists in a being-deleted state, or is already deleted
2308+
assert temporary_cloud_run_function is not None
2309+
try:
2310+
gcf = session.cloudfunctionsclient.get_function(
2311+
name=temporary_cloud_run_function
2312+
)
2313+
assert gcf.state is functions_v2.Function.State.DELETING
2314+
except google.cloud.exceptions.NotFound:
2315+
pass
2316+
finally:
2317+
# clean up the gcp assets created for the temporary remote function,
2318+
# just in case it was not explicitly cleaned up in the try clause due
2319+
# to assertion failure or exception earlier than that
2320+
cleanup_remote_function_assets(
2321+
session.bqclient, session.cloudfunctionsclient, add_one_remote_temp
2322+
)
2323+
2324+
2325+
@pytest.mark.parametrize(
2326+
("session_creator"),
2327+
[
2328+
pytest.param(bigframes.Session, id="session-constructor"),
2329+
pytest.param(bigframes.connect, id="connect-method"),
2330+
],
2331+
)
2332+
@pytest.mark.flaky(retries=2, delay=120)
2333+
def test_remote_function_w_context_manager_named(
2334+
scalars_dfs, dataset_id, bq_cf_connection, session_creator
2335+
):
2336+
def add_one(x: int) -> int:
2337+
return x + 1
2338+
2339+
scalars_df, scalars_pandas_df = scalars_dfs
2340+
pd_result = scalars_pandas_df["int64_too"].apply(add_one)
2341+
2342+
persistent_bigquery_remote_function = None
2343+
persistent_cloud_run_function = None
2344+
2345+
try:
2346+
with session_creator() as session:
2347+
# create a persistent remote function
2348+
name = test_utils.prefixer.Prefixer("bigframes", "").create_prefix()
2349+
add_one_remote_persist = session.remote_function(
2350+
dataset=dataset_id,
2351+
bigquery_connection=bq_cf_connection,
2352+
reuse=False,
2353+
name=name,
2354+
)(add_one)
2355+
2356+
persistent_bigquery_remote_function = (
2357+
add_one_remote_persist.bigframes_remote_function
2358+
)
2359+
assert persistent_bigquery_remote_function is not None
2360+
assert (
2361+
session.bqclient.get_routine(persistent_bigquery_remote_function)
2362+
is not None
2363+
)
2364+
2365+
persistent_cloud_run_function = (
2366+
add_one_remote_persist.bigframes_cloud_function
2367+
)
2368+
assert persistent_cloud_run_function is not None
2369+
assert (
2370+
session.cloudfunctionsclient.get_function(
2371+
name=persistent_cloud_run_function
2372+
)
2373+
is not None
2374+
)
2375+
2376+
bf_result = (
2377+
scalars_df["int64_too"].apply(add_one_remote_persist).to_pandas()
2378+
)
2379+
pandas.testing.assert_series_equal(bf_result, pd_result, check_dtype=False)
2380+
2381+
# outside the with statement context manager the persistent BQ remote
2382+
# function and the underlying cloud run function should still exist
2383+
assert persistent_bigquery_remote_function is not None
2384+
assert (
2385+
session.bqclient.get_routine(persistent_bigquery_remote_function)
2386+
is not None
2387+
)
2388+
assert persistent_cloud_run_function is not None
2389+
assert (
2390+
session.cloudfunctionsclient.get_function(
2391+
name=persistent_cloud_run_function
2392+
)
2393+
is not None
2394+
)
2395+
finally:
2396+
# clean up the gcp assets created for the persistent remote function
2397+
cleanup_remote_function_assets(
2398+
session.bqclient, session.cloudfunctionsclient, add_one_remote_persist
2399+
)

tests/system/large/test_session.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,38 @@ def test_clean_up_by_session_id():
133133
assert not any(
134134
[(session.session_id in table.full_table_id) for table in tables_after]
135135
)
136+
137+
138+
@pytest.mark.parametrize(
139+
("session_creator"),
140+
[
141+
pytest.param(bigframes.Session, id="session-constructor"),
142+
pytest.param(bigframes.connect, id="connect-method"),
143+
],
144+
)
145+
def test_clean_up_via_context_manager(session_creator):
146+
# we will create two tables and confirm that they are deleted
147+
# when the session is closed
148+
with session_creator() as session:
149+
bqclient = session.bqclient
150+
151+
expiration = (
152+
datetime.datetime.now(datetime.timezone.utc)
153+
+ bigframes.constants.DEFAULT_EXPIRATION
154+
)
155+
full_id_1 = bigframes.session._io.bigquery.create_temp_table(
156+
session.bqclient, session._temp_storage_manager._random_table(), expiration
157+
)
158+
full_id_2 = bigframes.session._io.bigquery.create_temp_table(
159+
session.bqclient, session._temp_storage_manager._random_table(), expiration
160+
)
161+
162+
# check that the tables were actually created
163+
assert bqclient.get_table(full_id_1).created is not None
164+
assert bqclient.get_table(full_id_2).created is not None
165+
166+
# check that the tables are already deleted
167+
with pytest.raises(google.cloud.exceptions.NotFound):
168+
bqclient.delete_table(full_id_1)
169+
with pytest.raises(google.cloud.exceptions.NotFound):
170+
bqclient.delete_table(full_id_2)

tests/unit/_config/test_bigquery_options.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,15 @@ def test_setter_if_session_started_but_setting_the_same_value(attribute):
9898
)
9999
def test_location_set_to_valid_no_warning(valid_location):
100100
# test setting location through constructor
101-
def set_location_in_ctor():
101+
def set_location_in_constructor():
102102
bigquery_options.BigQueryOptions(location=valid_location)
103103

104104
# test setting location property
105105
def set_location_property():
106106
options = bigquery_options.BigQueryOptions()
107107
options.location = valid_location
108108

109-
for op in [set_location_in_ctor, set_location_property]:
109+
for op in [set_location_in_constructor, set_location_property]:
110110
# Ensure that no warnings are emitted.
111111
# https://docs.pytest.org/en/7.0.x/how-to/capture-warnings.html#additional-use-cases-of-warnings-in-tests
112112
with warnings.catch_warnings():
@@ -136,15 +136,15 @@ def set_location_property():
136136
)
137137
def test_location_set_to_invalid_warning(invalid_location, possibility):
138138
# test setting location through constructor
139-
def set_location_in_ctor():
139+
def set_location_in_constructor():
140140
bigquery_options.BigQueryOptions(location=invalid_location)
141141

142142
# test setting location property
143143
def set_location_property():
144144
options = bigquery_options.BigQueryOptions()
145145
options.location = invalid_location
146146

147-
for op in [set_location_in_ctor, set_location_property]:
147+
for op in [set_location_in_constructor, set_location_property]:
148148
with pytest.warns(
149149
bigframes.exceptions.UnknownLocationWarning,
150150
match=re.escape(

0 commit comments

Comments
 (0)