From 487c5a22ecba66f1b945ed129120c6be2e246306 Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Mon, 30 Mar 2026 12:35:23 -0500 Subject: [PATCH 01/11] Add exception handling Signed-off-by: Bob Haddleton --- function/fn.py | 40 +++++++++----- tests/test_fn.py | 132 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 13 deletions(-) diff --git a/function/fn.py b/function/fn.py index d2b9689..c33bced 100644 --- a/function/fn.py +++ b/function/fn.py @@ -2,6 +2,8 @@ import importlib.util import inspect +import sys +import traceback import types import grpc @@ -24,12 +26,12 @@ async def RunFunction( ) -> fnv1.RunFunctionResponse: """Run the function.""" log = self.log.bind(tag=req.meta.tag) - log.info("Running function") + log.debug("Running function") rsp = response.to(req) - if req.input["script"] is None: - response.fatal(rsp, "missing script") + if "script" not in req.input or not req.input["script"]: + response.fatal(rsp, "missing script in function input") return rsp log.debug("Running script", script=req.input["script"]) @@ -44,22 +46,34 @@ async def RunFunction( log.debug(msg) response.fatal(rsp, msg) case (True, False): - log.debug("running composition function") - if inspect.iscoroutinefunction(script.compose): - await script.compose(req, rsp) - else: - script.compose(req, rsp) + try: + log.debug("running composition function") + if inspect.iscoroutinefunction(script.compose): + await script.compose(req, rsp) + else: + script.compose(req, rsp) + except Exception as e: + msg = f"Exception: {type(e)}, traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + log.debug(msg) + response.fatal(rsp, msg) + case (False, True): log.debug("running operation function") - if inspect.iscoroutinefunction(script.operate): - await script.operate(req, rsp) - else: - script.operate(req, rsp) + try: + if inspect.iscoroutinefunction(script.operate): + await script.operate(req, rsp) + else: + script.operate(req, rsp) + except Exception as e: + msg = f"Exception: {e}, traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + log.debug(msg) + response.fatal(rsp, msg) + case (False, False): msg = "script must define a compose or operate function" log.debug(msg) response.fatal(rsp, msg) - + log.debug(f"Response: {rsp}") return rsp diff --git a/tests/test_fn.py b/tests/test_fn.py index aa9da1b..3598975 100644 --- a/tests/test_fn.py +++ b/tests/test_fn.py @@ -24,6 +24,22 @@ def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse): }) """ +composition_script_with_exception = """ +from crossplane.function.proto.v1 import run_function_pb2 as fnv1 + +def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse): + rsp.desired.resources["bucket"].resource.update({ + "apiVersion": "s3.aws.upbound.io/v1beta2", + "kind": "Bucket", + "spec": { + "forProvider": { + "region": "us-east-1" + } + }, + }) + raise AttributeError +""" + async_composition_script = """ from crossplane.function.proto.v1 import run_function_pb2 as fnv1 @@ -39,6 +55,22 @@ async def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse): }) """ +async_composition_script_with_exception = """ +from crossplane.function.proto.v1 import run_function_pb2 as fnv1 + +async def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse): + rsp.desired.resources["bucket"].resource.update({ + "apiVersion": "s3.aws.upbound.io/v1beta2", + "kind": "Bucket", + "spec": { + "forProvider": { + "region": "us-east-1" + } + }, + }) + raise AttributeError +""" + operation_script = """ from crossplane.function.proto.v1 import run_function_pb2 as fnv1 @@ -140,6 +172,106 @@ class TestCase: context=structpb.Struct(), ), ), + TestCase( + reason="Function should fail gracefully when script is missing.", + req=fnv1.RunFunctionRequest( + input=resource.dict_to_struct({}), + ), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), + results=[ + { + "message": "missing script in function input", + "severity": "SEVERITY_FATAL", + } + ], + desired=fnv1.State(), + context=structpb.Struct(), + ), + ), + TestCase( + reason="Function should fail gracefully when script is empty.", + req=fnv1.RunFunctionRequest( + input=resource.dict_to_struct({"script": ""}), + ), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), + results=[ + { + "message": "missing script in function input", + "severity": "SEVERITY_FATAL", + } + ], + desired=fnv1.State(), + context=structpb.Struct(), + ), + ), + TestCase( + reason="Function should fail gracefully when compose script raises an exception.", + req=fnv1.RunFunctionRequest( + input=resource.dict_to_struct( + {"script": composition_script_with_exception} + ), + ), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), + results=[ + { + "message": "Exception: , traceback: [' File \"\", line 14, in compose\\n']", + "severity": "SEVERITY_FATAL", + } + ], + desired=fnv1.State( + resources={ + "bucket": fnv1.Resource( + resource=resource.dict_to_struct( + { + "apiVersion": "s3.aws.upbound.io/v1beta2", + "kind": "Bucket", + "spec": { + "forProvider": {"region": "us-east-1"} + }, + } + ) + ) + } + ), + context=structpb.Struct(), + ), + ), + TestCase( + reason="Function should fail gracefully when async compose script raises an exception.", + req=fnv1.RunFunctionRequest( + input=resource.dict_to_struct( + {"script": async_composition_script_with_exception} + ), + ), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), + results=[ + { + "message": "Exception: , traceback: [' File \"\", line 14, in compose\\n']", + "severity": "SEVERITY_FATAL", + } + ], + desired=fnv1.State( + resources={ + "bucket": fnv1.Resource( + resource=resource.dict_to_struct( + { + "apiVersion": "s3.aws.upbound.io/v1beta2", + "kind": "Bucket", + "spec": { + "forProvider": {"region": "us-east-1"} + }, + } + ) + ) + } + ), + context=structpb.Struct(), + ), + ), ] runner = fn.FunctionRunner() From 881f96d44d464ee8053ed32f7bb7ce6059ef8d4e Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Mon, 30 Mar 2026 12:48:36 -0500 Subject: [PATCH 02/11] Fix lint errors Signed-off-by: Bob Haddleton --- function/fn.py | 11 ++++++++--- tests/test_fn.py | 14 ++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/function/fn.py b/function/fn.py index c33bced..96edc19 100644 --- a/function/fn.py +++ b/function/fn.py @@ -2,7 +2,6 @@ import importlib.util import inspect -import sys import traceback import types @@ -53,7 +52,10 @@ async def RunFunction( else: script.compose(req, rsp) except Exception as e: - msg = f"Exception: {type(e)}, traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + msg = ( + f"Exception: {type(e)}, " + f"traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + ) log.debug(msg) response.fatal(rsp, msg) @@ -65,7 +67,10 @@ async def RunFunction( else: script.operate(req, rsp) except Exception as e: - msg = f"Exception: {e}, traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + msg = ( + f"Exception: {e}, " + f"traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + ) log.debug(msg) response.fatal(rsp, msg) diff --git a/tests/test_fn.py b/tests/test_fn.py index 3598975..4d3603f 100644 --- a/tests/test_fn.py +++ b/tests/test_fn.py @@ -207,7 +207,8 @@ class TestCase: ), ), TestCase( - reason="Function should fail gracefully when compose script raises an exception.", + reason="Function should fail gracefully when compose script raises" + " an exception.", req=fnv1.RunFunctionRequest( input=resource.dict_to_struct( {"script": composition_script_with_exception} @@ -217,7 +218,9 @@ class TestCase: meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), results=[ { - "message": "Exception: , traceback: [' File \"\", line 14, in compose\\n']", + "message": "Exception: , traceback:" + " [' File \"\", line 14, in compose\\n'" + "]", "severity": "SEVERITY_FATAL", } ], @@ -240,7 +243,8 @@ class TestCase: ), ), TestCase( - reason="Function should fail gracefully when async compose script raises an exception.", + reason="Function should fail gracefully when async compose script" + " raises an exception.", req=fnv1.RunFunctionRequest( input=resource.dict_to_struct( {"script": async_composition_script_with_exception} @@ -250,7 +254,9 @@ class TestCase: meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), results=[ { - "message": "Exception: , traceback: [' File \"\", line 14, in compose\\n']", + "message": "Exception: , traceback:" + " [' File \"\", line 14, in compose\\n'" + "]", "severity": "SEVERITY_FATAL", } ], From f1652e1de0f5ee0b76266e21800e1cb4632cd873 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:21:48 +0000 Subject: [PATCH 03/11] fix(deps): update dependency click to v8.3.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 28f4e32..a573be6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ dependencies = [ "crossplane-function-sdk-python==0.11.0", - "click==8.3.1", + "click==8.3.2", "grpcio==1.76.0", "aioboto3==15.5.0", ] From 0a02d57666bec6c866876a9018df8e8ca709f02f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 05:45:26 +0000 Subject: [PATCH 04/11] fix(deps): update dependency click to v8.4.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a573be6..e8cd76a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ dependencies = [ "crossplane-function-sdk-python==0.11.0", - "click==8.3.2", + "click==8.4.1", "grpcio==1.76.0", "aioboto3==15.5.0", ] From 9e41ef372ef49d9fe4010566bb3eb6c1c61e67ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:21:00 +0000 Subject: [PATCH 05/11] fix(deps): update dependency crossplane-function-sdk-python to v0.14.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e8cd76a..e5459b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ ] dependencies = [ - "crossplane-function-sdk-python==0.11.0", + "crossplane-function-sdk-python==0.14.0", "click==8.4.1", "grpcio==1.76.0", "aioboto3==15.5.0", From 5904c2b2fd30910fd2e877658d03a2c7bdb361bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:13:35 +0000 Subject: [PATCH 06/11] chore(deps): update actions/checkout action to v7 --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e23f054..a2b242e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup Python uses: actions/setup-python@v6 @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup Python uses: actions/setup-python@v6 @@ -94,7 +94,7 @@ jobs: install: true - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 # We ask Docker to use GitHub Action's native caching support to speed up # the build, per https://docs.docker.com/build/cache/backends/gha/. @@ -134,7 +134,7 @@ jobs: - build steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Download Single-Platform Packages uses: actions/download-artifact@v8 From 3e518004d881014e91e7ad92e14041152376d0fe Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Thu, 18 Jun 2026 16:31:03 -0500 Subject: [PATCH 07/11] Update grpcio dependency Signed-off-by: Bob Haddleton --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e5459b0..4427f5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ dependencies = [ "crossplane-function-sdk-python==0.14.0", "click==8.4.1", - "grpcio==1.76.0", + "grpcio>=1.76.0", "aioboto3==15.5.0", ] From ac3e5fb0e8b9692267a15140a1b21229e6b3f0a7 Mon Sep 17 00:00:00 2001 From: Yury Tsarev Date: Tue, 23 Jun 2026 23:38:39 +0200 Subject: [PATCH 08/11] Add Yury to maintainers Signed-off-by: Yury Tsarev --- OWNERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/OWNERS.md b/OWNERS.md index e65bf3a..96492f8 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -4,3 +4,4 @@ * Nic Cope ([negz](https://github.com/negz)) * Bob Haddleton ([bobh66](https://github.com/bobh66)) +* Yury Tsarev ([ytsarev](https://github.com/ytsarev)) From 4e432875e9498ce127a5f071e1e9bcbd5cef18a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:52:59 +0000 Subject: [PATCH 09/11] fix(deps): update dependency click to v8.4.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4427f5e..1719c1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ dependencies = [ "crossplane-function-sdk-python==0.14.0", - "click==8.4.1", + "click==8.4.2", "grpcio>=1.76.0", "aioboto3==15.5.0", ] From fd46a02894731641b59185c506719dbc534133c6 Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Mon, 30 Mar 2026 12:35:23 -0500 Subject: [PATCH 10/11] Add exception handling Signed-off-by: Bob Haddleton --- function/fn.py | 40 +++++++++----- tests/test_fn.py | 132 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 13 deletions(-) diff --git a/function/fn.py b/function/fn.py index d2b9689..c33bced 100644 --- a/function/fn.py +++ b/function/fn.py @@ -2,6 +2,8 @@ import importlib.util import inspect +import sys +import traceback import types import grpc @@ -24,12 +26,12 @@ async def RunFunction( ) -> fnv1.RunFunctionResponse: """Run the function.""" log = self.log.bind(tag=req.meta.tag) - log.info("Running function") + log.debug("Running function") rsp = response.to(req) - if req.input["script"] is None: - response.fatal(rsp, "missing script") + if "script" not in req.input or not req.input["script"]: + response.fatal(rsp, "missing script in function input") return rsp log.debug("Running script", script=req.input["script"]) @@ -44,22 +46,34 @@ async def RunFunction( log.debug(msg) response.fatal(rsp, msg) case (True, False): - log.debug("running composition function") - if inspect.iscoroutinefunction(script.compose): - await script.compose(req, rsp) - else: - script.compose(req, rsp) + try: + log.debug("running composition function") + if inspect.iscoroutinefunction(script.compose): + await script.compose(req, rsp) + else: + script.compose(req, rsp) + except Exception as e: + msg = f"Exception: {type(e)}, traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + log.debug(msg) + response.fatal(rsp, msg) + case (False, True): log.debug("running operation function") - if inspect.iscoroutinefunction(script.operate): - await script.operate(req, rsp) - else: - script.operate(req, rsp) + try: + if inspect.iscoroutinefunction(script.operate): + await script.operate(req, rsp) + else: + script.operate(req, rsp) + except Exception as e: + msg = f"Exception: {e}, traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + log.debug(msg) + response.fatal(rsp, msg) + case (False, False): msg = "script must define a compose or operate function" log.debug(msg) response.fatal(rsp, msg) - + log.debug(f"Response: {rsp}") return rsp diff --git a/tests/test_fn.py b/tests/test_fn.py index aa9da1b..3598975 100644 --- a/tests/test_fn.py +++ b/tests/test_fn.py @@ -24,6 +24,22 @@ def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse): }) """ +composition_script_with_exception = """ +from crossplane.function.proto.v1 import run_function_pb2 as fnv1 + +def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse): + rsp.desired.resources["bucket"].resource.update({ + "apiVersion": "s3.aws.upbound.io/v1beta2", + "kind": "Bucket", + "spec": { + "forProvider": { + "region": "us-east-1" + } + }, + }) + raise AttributeError +""" + async_composition_script = """ from crossplane.function.proto.v1 import run_function_pb2 as fnv1 @@ -39,6 +55,22 @@ async def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse): }) """ +async_composition_script_with_exception = """ +from crossplane.function.proto.v1 import run_function_pb2 as fnv1 + +async def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse): + rsp.desired.resources["bucket"].resource.update({ + "apiVersion": "s3.aws.upbound.io/v1beta2", + "kind": "Bucket", + "spec": { + "forProvider": { + "region": "us-east-1" + } + }, + }) + raise AttributeError +""" + operation_script = """ from crossplane.function.proto.v1 import run_function_pb2 as fnv1 @@ -140,6 +172,106 @@ class TestCase: context=structpb.Struct(), ), ), + TestCase( + reason="Function should fail gracefully when script is missing.", + req=fnv1.RunFunctionRequest( + input=resource.dict_to_struct({}), + ), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), + results=[ + { + "message": "missing script in function input", + "severity": "SEVERITY_FATAL", + } + ], + desired=fnv1.State(), + context=structpb.Struct(), + ), + ), + TestCase( + reason="Function should fail gracefully when script is empty.", + req=fnv1.RunFunctionRequest( + input=resource.dict_to_struct({"script": ""}), + ), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), + results=[ + { + "message": "missing script in function input", + "severity": "SEVERITY_FATAL", + } + ], + desired=fnv1.State(), + context=structpb.Struct(), + ), + ), + TestCase( + reason="Function should fail gracefully when compose script raises an exception.", + req=fnv1.RunFunctionRequest( + input=resource.dict_to_struct( + {"script": composition_script_with_exception} + ), + ), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), + results=[ + { + "message": "Exception: , traceback: [' File \"\", line 14, in compose\\n']", + "severity": "SEVERITY_FATAL", + } + ], + desired=fnv1.State( + resources={ + "bucket": fnv1.Resource( + resource=resource.dict_to_struct( + { + "apiVersion": "s3.aws.upbound.io/v1beta2", + "kind": "Bucket", + "spec": { + "forProvider": {"region": "us-east-1"} + }, + } + ) + ) + } + ), + context=structpb.Struct(), + ), + ), + TestCase( + reason="Function should fail gracefully when async compose script raises an exception.", + req=fnv1.RunFunctionRequest( + input=resource.dict_to_struct( + {"script": async_composition_script_with_exception} + ), + ), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), + results=[ + { + "message": "Exception: , traceback: [' File \"\", line 14, in compose\\n']", + "severity": "SEVERITY_FATAL", + } + ], + desired=fnv1.State( + resources={ + "bucket": fnv1.Resource( + resource=resource.dict_to_struct( + { + "apiVersion": "s3.aws.upbound.io/v1beta2", + "kind": "Bucket", + "spec": { + "forProvider": {"region": "us-east-1"} + }, + } + ) + ) + } + ), + context=structpb.Struct(), + ), + ), ] runner = fn.FunctionRunner() From 2b2aaedfaf5ba7fbe827a0472dcb0821effba4fd Mon Sep 17 00:00:00 2001 From: Bob Haddleton Date: Mon, 30 Mar 2026 12:48:36 -0500 Subject: [PATCH 11/11] Fix lint errors Signed-off-by: Bob Haddleton --- function/fn.py | 11 ++++++++--- tests/test_fn.py | 14 ++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/function/fn.py b/function/fn.py index c33bced..96edc19 100644 --- a/function/fn.py +++ b/function/fn.py @@ -2,7 +2,6 @@ import importlib.util import inspect -import sys import traceback import types @@ -53,7 +52,10 @@ async def RunFunction( else: script.compose(req, rsp) except Exception as e: - msg = f"Exception: {type(e)}, traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + msg = ( + f"Exception: {type(e)}, " + f"traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + ) log.debug(msg) response.fatal(rsp, msg) @@ -65,7 +67,10 @@ async def RunFunction( else: script.operate(req, rsp) except Exception as e: - msg = f"Exception: {e}, traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + msg = ( + f"Exception: {e}, " + f"traceback: {traceback.format_tb(e.__traceback__.tb_next)}" + ) log.debug(msg) response.fatal(rsp, msg) diff --git a/tests/test_fn.py b/tests/test_fn.py index 3598975..4d3603f 100644 --- a/tests/test_fn.py +++ b/tests/test_fn.py @@ -207,7 +207,8 @@ class TestCase: ), ), TestCase( - reason="Function should fail gracefully when compose script raises an exception.", + reason="Function should fail gracefully when compose script raises" + " an exception.", req=fnv1.RunFunctionRequest( input=resource.dict_to_struct( {"script": composition_script_with_exception} @@ -217,7 +218,9 @@ class TestCase: meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), results=[ { - "message": "Exception: , traceback: [' File \"\", line 14, in compose\\n']", + "message": "Exception: , traceback:" + " [' File \"\", line 14, in compose\\n'" + "]", "severity": "SEVERITY_FATAL", } ], @@ -240,7 +243,8 @@ class TestCase: ), ), TestCase( - reason="Function should fail gracefully when async compose script raises an exception.", + reason="Function should fail gracefully when async compose script" + " raises an exception.", req=fnv1.RunFunctionRequest( input=resource.dict_to_struct( {"script": async_composition_script_with_exception} @@ -250,7 +254,9 @@ class TestCase: meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)), results=[ { - "message": "Exception: , traceback: [' File \"\", line 14, in compose\\n']", + "message": "Exception: , traceback:" + " [' File \"\", line 14, in compose\\n'" + "]", "severity": "SEVERITY_FATAL", } ],