From efb0e84d3f8ada6ac305b216baa6632570c38495 Mon Sep 17 00:00:00 2001 From: Annie Fu <16651409+anniefu@users.noreply.github.com> Date: Wed, 1 Dec 2021 11:26:26 -0800 Subject: [PATCH 01/11] docs: update README to use declarative function signatures (#171) * docs: update README to use declarative function signatures Reduce redundancy in quickstart examples. * Fix newline --- README.md | 88 +++++++++++++++++++++---------------------------------- 1 file changed, 33 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 029620ce..192a9b55 100644 --- a/README.md +++ b/README.md @@ -49,16 +49,19 @@ pip install functions-framework Or, for deployment, add the Functions Framework to your `requirements.txt` file: ``` -functions-framework==2.3.0 +functions-framework==3.* ``` ## Quickstarts -### Quickstart: Hello, World on your local machine +### Quickstart: HTTP Function (Hello World) Create an `main.py` file with the following contents: ```python +import functions_framework + +@functions_framework.http def hello(request): return "Hello world!" ``` @@ -67,30 +70,6 @@ def hello(request): Run the following command: -```sh -functions-framework --target=hello -``` - -Open http://localhost:8080/ in your browser and see *Hello world!*. - - -### Quickstart: Set up a new project - -Create a `main.py` file with the following contents: - -```python -def hello(request): - return "Hello world!" -``` - -Now install the Functions Framework: - -```sh -pip install functions-framework -``` - -Use the `functions-framework` command to start the built-in local development server: - ```sh functions-framework --target hello --debug * Serving Flask app "hello" (lazy loading) @@ -101,17 +80,19 @@ functions-framework --target hello --debug * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit) ``` -(You can also use `functions-framework-python` if you potentially have multiple +(You can also use `functions-framework-python` if you have multiple language frameworks installed). -Send requests to this function using `curl` from another terminal window: +Open http://localhost:8080/ in your browser and see *Hello world!*. + +Or send requests to this function using `curl` from another terminal window: ```sh curl localhost:8080 # Output: Hello world! ``` -### Quickstart: Register your function using decorator +### Quickstart: CloudEvent Function Create an `main.py` file with the following contents: @@ -120,27 +101,37 @@ import functions_framework @functions_framework.cloud_event def hello_cloud_event(cloud_event): - return f"Received event with ID: {cloud_event['id']} and data {cloud_event.data}" - -@functions_framework.http -def hello_http(request): - return "Hello world!" - + print(f"Received event with ID: {cloud_event['id']} and data {cloud_event.data}") ``` -Run the following command to run `hello_http` target locally: +> Your function is passed a single [CloudEvent](https://github.com/cloudevents/sdk-python/blob/master/cloudevents/sdk/event/v1.py) parameter. + +Run the following command to run `hello_cloud_event` target locally: ```sh -functions-framework --target=hello_http +functions-framework --target=hello_cloud_event ``` -Open http://localhost:8080/ in your browser and see *Hello world!*. - -Run the following command to run `hello_cloud_event` target locally: +In a different terminal, `curl` the Functions Framework server: ```sh -functions-framework --target=hello_cloud_event +curl -X POST localhost:8080 \ + -H "Content-Type: application/cloudevents+json" \ + -d '{ + "specversion" : "1.0", + "type" : "example.com.cloud.event", + "source" : "https://example.com/cloudevents/pull", + "subject" : "123", + "id" : "A234-1234-1234", + "time" : "2018-04-05T17:31:00Z", + "data" : "hello world" +}' +``` + +Output from the terminal running `functions-framework`: ``` +Received event with ID: A234-1234-1234 and data hello world +``` More info on sending [CloudEvents](http://cloudevents.io) payloads, see [`examples/cloud_run_cloud_events`](examples/cloud_run_cloud_events/) instruction. @@ -333,7 +324,7 @@ You can configure the Functions Framework using command-line flags or environmen | `--debug` | `DEBUG` | A flag that allows to run functions-framework to run in debug mode, including live reloading. Default: `False` | | `--dry-run` | `DRY_RUN` | A flag that allows for testing the function build from the configuration without creating a server. Default: `False` | -## Enable Google Cloud Functions Events +## Enable Google Cloud Function Events The Functions Framework can unmarshall incoming Google Cloud Functions [event](https://cloud.google.com/functions/docs/concepts/events-triggers#events) payloads to `event` and `context` objects. @@ -356,19 +347,6 @@ documentation on See the [running example](examples/cloud_run_event). -## Enable CloudEvents - -The Functions framework can also unmarshall incoming [CloudEvents](http://cloudevents.io) payloads to the `cloud_event` object. This will be passed as a [CloudEvent](https://github.com/cloudevents/sdk-python) to your function when it receives a request. Note that your function must use the `CloudEvents`-style function signature: - -```python -def hello(cloud_event): - print(f"Received event with ID: {cloud_event['id']}") -``` - -To enable automatic unmarshalling, set the function signature type to `cloudevent` using the `--signature-type` command-line flag or the `FUNCTION_SIGNATURE_TYPE` environment variable. By default, the HTTP signature type will be used and automatic event unmarshalling will be disabled. - -For more details on this signature type, check out the Google Cloud Functions documentation on [background functions](https://cloud.google.com/functions/docs/writing/background#cloud_pubsub_example). - ## Advanced Examples More advanced guides can be found in the [`examples/`](examples/) directory. From 6f4a3608634debe0833d0ef5cd769050b5fecb01 Mon Sep 17 00:00:00 2001 From: Arjun Srinivasan <69502+asriniva@users.noreply.github.com> Date: Wed, 8 Dec 2021 09:58:07 -0800 Subject: [PATCH 02/11] fix: Change gunicorn request line limit to unlimited (#173) --- src/functions_framework/_http/gunicorn.py | 1 + tests/test_http.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/functions_framework/_http/gunicorn.py b/src/functions_framework/_http/gunicorn.py index 48714284..25fdb790 100644 --- a/src/functions_framework/_http/gunicorn.py +++ b/src/functions_framework/_http/gunicorn.py @@ -23,6 +23,7 @@ def __init__(self, app, host, port, debug, **options): "threads": 8, "timeout": 0, "loglevel": "error", + "limit_request_line": 0, } self.options.update(options) self.app = app diff --git a/tests/test_http.py b/tests/test_http.py index e45dbab9..bd301c91 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -100,6 +100,7 @@ def test_gunicorn_application(debug): "threads": 8, "timeout": 0, "loglevel": "error", + "limit_request_line": 0, } assert gunicorn_app.cfg.bind == ["1.2.3.4:1234"] From 9046388fe8c32e897b83315863ee57ccf7d0e8df Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Tue, 28 Dec 2021 17:03:01 -0500 Subject: [PATCH 03/11] fix: Support relative imports for submodules (#169) --- src/functions_framework/_function_registry.py | 4 +++- tests/test_functions.py | 11 ++++++++++ tests/test_functions/relative_imports/main.py | 22 +++++++++++++++++++ tests/test_functions/relative_imports/test.py | 1 + 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/test_functions/relative_imports/main.py create mode 100644 tests/test_functions/relative_imports/test.py diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index f7869b24..cedb7e15 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -62,7 +62,9 @@ def load_function_module(source): directory, filename = os.path.split(realpath) name, extension = os.path.splitext(filename) # 2. Create a new module - spec = importlib.util.spec_from_file_location(name, realpath) + spec = importlib.util.spec_from_file_location( + name, realpath, submodule_search_locations=[directory] + ) source_module = importlib.util.module_from_spec(spec) # 3. Add the directory of the source to sys.path to allow the function to # load modules relative to its location diff --git a/tests/test_functions.py b/tests/test_functions.py index 64ecc794..c343205f 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -604,3 +604,14 @@ def tests_cloud_to_background_event_client_invalid_source( resp = background_event_client.post("/", headers=headers, json=tempfile_payload) assert resp.status_code == 500 + + +def test_relative_imports(): + source = TEST_FUNCTIONS_DIR / "relative_imports" / "main.py" + target = "function" + + client = create_app(target, source).test_client() + + resp = client.get("/") + assert resp.status_code == 200 + assert resp.data == b"success" diff --git a/tests/test_functions/relative_imports/main.py b/tests/test_functions/relative_imports/main.py new file mode 100644 index 00000000..3c28ff1a --- /dev/null +++ b/tests/test_functions/relative_imports/main.py @@ -0,0 +1,22 @@ +# Copyright 2020 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. + +"""Function used in test for relative imports.""" + +from .test import foo + + +def function(request): + """Test HTTP function who returns a value from a relative import""" + return foo diff --git a/tests/test_functions/relative_imports/test.py b/tests/test_functions/relative_imports/test.py new file mode 100644 index 00000000..862c735d --- /dev/null +++ b/tests/test_functions/relative_imports/test.py @@ -0,0 +1 @@ +foo = "success" From abef66464c8cc8dc51f7b7d94a856602594a06f5 Mon Sep 17 00:00:00 2001 From: Annie Fu <16651409+anniefu@users.noreply.github.com> Date: Thu, 28 Apr 2022 09:33:29 -0700 Subject: [PATCH 04/11] test: fix unit tests (#181) Fix failing assert to look for a different error message, likely due to a change in CloudEvents error message. Also add tests so that code coverage is back at 100%. --- tests/test_cloud_event_functions.py | 2 +- tests/test_convert.py | 8 ++++++++ tests/test_view_functions.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_cloud_event_functions.py b/tests/test_cloud_event_functions.py index 4ad8a527..a673c6e4 100644 --- a/tests/test_cloud_event_functions.py +++ b/tests/test_cloud_event_functions.py @@ -190,7 +190,7 @@ def test_unparsable_cloud_event(client): resp = client.post("/", headers={}, data="") assert resp.status_code == 400 - assert "MissingRequiredFields" in resp.data.decode() + assert "Bad Request" in resp.data.decode() @pytest.mark.parametrize("specversion", ["0.3", "1.0"]) diff --git a/tests/test_convert.py b/tests/test_convert.py index 9202f567..0d41d5ed 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -15,6 +15,7 @@ import pathlib import flask +import pretend import pytest from cloudevents.http import from_json, to_binary @@ -259,6 +260,13 @@ def test_firebase_db_event_to_cloud_event_missing_domain( ) +def test_marshal_background_event_data_bad_request(): + req = pretend.stub(headers={}, get_json=lambda: None) + + with pytest.raises(EventConversionException): + event_conversion.background_event_to_cloud_event(req) + + @pytest.mark.parametrize( "background_resource", [ diff --git a/tests/test_view_functions.py b/tests/test_view_functions.py index 219313f9..8de543d1 100644 --- a/tests/test_view_functions.py +++ b/tests/test_view_functions.py @@ -14,6 +14,8 @@ import json import pretend +import pytest +import werkzeug from cloudevents.http import from_http @@ -63,6 +65,20 @@ def test_event_view_func_wrapper(monkeypatch): ] +def test_event_view_func_wrapper_bad_request(monkeypatch): + request = pretend.stub(headers={}, get_json=lambda: None) + + context_stub = pretend.stub() + context_class = pretend.call_recorder(lambda *a, **kw: context_stub) + monkeypatch.setattr(functions_framework, "Context", context_class) + function = pretend.call_recorder(lambda data, context: "Hello") + + view_func = functions_framework._event_view_func_wrapper(function, request) + + with pytest.raises(werkzeug.exceptions.BadRequest): + view_func("/some/path") + + def test_run_cloud_event(): headers = {"Content-Type": "application/cloudevents+json"} data = json.dumps( From 0a7937c37bfbe7ced5624264ef426ac8225c617b Mon Sep 17 00:00:00 2001 From: Annie Fu <16651409+anniefu@users.noreply.github.com> Date: Fri, 29 Apr 2022 09:06:33 -0700 Subject: [PATCH 05/11] chore: use Release Please app for releases (#182) This also handles bumping the version number in `setup.py` so that a separate PR doesn't need to made. When the GitHub Release is published by the Release Please app, it will kick off the "Release to PyPI" Workflow. --- .github/release-please.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/release-please.yml diff --git a/.github/release-please.yml b/.github/release-please.yml new file mode 100644 index 00000000..30c96e19 --- /dev/null +++ b/.github/release-please.yml @@ -0,0 +1,2 @@ +releaseType: python +handleGHRelease: true \ No newline at end of file From 6c9ce8c0e36d0c3752a40060c06e2162641aa805 Mon Sep 17 00:00:00 2001 From: Annie Fu <16651409+anniefu@users.noreply.github.com> Date: Fri, 29 Apr 2022 13:20:50 -0700 Subject: [PATCH 06/11] chore: add GCF buildpack integration test Workflow (#185) See [functions-framework-conformance builidpack integration workflow PR](https://github.com/GoogleCloudPlatform/functions-framework-conformance/pull/99) for more information. --- .../workflows/buildpack-integration-test.yml | 52 +++++++++++++++++++ tests/conformance/prerun.sh | 21 ++++++++ 2 files changed, 73 insertions(+) create mode 100644 .github/workflows/buildpack-integration-test.yml create mode 100755 tests/conformance/prerun.sh diff --git a/.github/workflows/buildpack-integration-test.yml b/.github/workflows/buildpack-integration-test.yml new file mode 100644 index 00000000..7ca8e38b --- /dev/null +++ b/.github/workflows/buildpack-integration-test.yml @@ -0,0 +1,52 @@ +# Validates Functions Framework with GCF buildpacks. +name: Buildpack Integration Test +on: + push: + branches: + - master + workflow_dispatch: +jobs: + python37: + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.4.1 + with: + http-builder-source: 'tests/conformance' + http-builder-target: 'write_http_declarative' + cloudevent-builder-source: 'tests/conformance' + cloudevent-builder-target: 'write_cloud_event_declarative' + prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' + builder-runtime: 'python37' + # Latest uploaded tag from us.gcr.io/fn-img/buildpacks/python37/builder + builder-tag: 'python37_20220426_3_7_12_RC00' + python38: + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.4.1 + with: + http-builder-source: 'tests/conformance' + http-builder-target: 'write_http_declarative' + cloudevent-builder-source: 'tests/conformance' + cloudevent-builder-target: 'write_cloud_event_declarative' + prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' + builder-runtime: 'python38' + # Latest uploaded tag from us.gcr.io/fn-img/buildpacks/python38/builder + builder-tag: 'python38_20220426_3_8_12_RC00' + python39: + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.4.1 + with: + http-builder-source: 'tests/conformance' + http-builder-target: 'write_http_declarative' + cloudevent-builder-source: 'tests/conformance' + cloudevent-builder-target: 'write_cloud_event_declarative' + prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' + builder-runtime: 'python39' + # Latest uploaded tag from us.gcr.io/fn-img/buildpacks/python39/builder + builder-tag: 'python39_20220426_3_9_10_RC00' + python310: + uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@v1.4.1 + with: + http-builder-source: 'tests/conformance' + http-builder-target: 'write_http_declarative' + cloudevent-builder-source: 'tests/conformance' + cloudevent-builder-target: 'write_cloud_event_declarative' + prerun: 'tests/conformance/prerun.sh ${{ github.sha }}' + builder-runtime: 'python310' + # Latest uploaded tag from us.gcr.io/fn-img/buildpacks/python310/builder + builder-tag: 'python310_20220320_3_10_2_RC00' \ No newline at end of file diff --git a/tests/conformance/prerun.sh b/tests/conformance/prerun.sh new file mode 100755 index 00000000..b46f0b51 --- /dev/null +++ b/tests/conformance/prerun.sh @@ -0,0 +1,21 @@ +# prerun.sh sets up the test function to use the functions framework commit +# specified by generating a `requirements.txt`. This makes the function `pack` buildable +# with GCF buildpacks. +# +# `pack` command example: +# pack build python-test --builder us.gcr.io/fn-img/buildpacks/python310/builder:python310_20220320_3_10_2_RC00 --env GOOGLE_RUNTIME=python310 --env GOOGLE_FUNCTION_TARGET=write_http_declarative +set -e + +FRAMEWORK_VERSION=$1 +if [ -z "${FRAMEWORK_VERSION}" ] + then + echo "Functions Framework version required as first parameter" + exit 1 +fi + +SCRIPT_DIR=$(realpath $(dirname $0)) + +cd $SCRIPT_DIR + +echo "git+https://github.com/GoogleCloudPlatform/functions-framework-python@$FRAMEWORK_VERSION#egg=functions-framework" > requirements.txt +cat requirements.txt \ No newline at end of file From f2285f96072d7ab8f00ada0d5f8c075e2b4ad364 Mon Sep 17 00:00:00 2001 From: Sander van Leeuwen Date: Tue, 10 May 2022 22:37:32 +0200 Subject: [PATCH 07/11] fix: Add functools.wraps decorator (#179) * fix: Add functools.wraps decorator (#178) To make sure function attributes are copied to `_http_view_func_wrapper` Co-authored-by: Annie Fu <16651409+anniefu@users.noreply.github.com> --- src/functions_framework/__init__.py | 1 + tests/test_view_functions.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py index 46c8882b..5d18d2ab 100644 --- a/src/functions_framework/__init__.py +++ b/src/functions_framework/__init__.py @@ -95,6 +95,7 @@ def setup_logging(): def _http_view_func_wrapper(function, request): + @functools.wraps(function) def view_func(path): return function(request._get_current_object()) diff --git a/tests/test_view_functions.py b/tests/test_view_functions.py index 8de543d1..f69b2155 100644 --- a/tests/test_view_functions.py +++ b/tests/test_view_functions.py @@ -33,6 +33,17 @@ def test_http_view_func_wrapper(): assert function.calls == [pretend.call(request_object)] +def test_http_view_func_wrapper_attribute_copied(): + def function(_): + pass + + function.attribute = "foo" + view_func = functions_framework._http_view_func_wrapper(function, pretend.stub()) + + assert view_func.__name__ == "function" + assert view_func.attribute == "foo" + + def test_event_view_func_wrapper(monkeypatch): data = pretend.stub() json = { From a820fd4cdb4bef6cffe0ef68a2d03af922f13d7e Mon Sep 17 00:00:00 2001 From: MikeM Date: Wed, 18 May 2022 06:50:54 +0100 Subject: [PATCH 08/11] fix: for issue #170 gracefully handle pubsub messages without attributes in them (#187) * Handle when attributes not present Co-authored-by: Annie Fu <16651409+anniefu@users.noreply.github.com> --- src/functions_framework/event_conversion.py | 2 +- tests/test_convert.py | 47 +++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/functions_framework/event_conversion.py b/src/functions_framework/event_conversion.py index 28cf2a1b..06e5a812 100644 --- a/src/functions_framework/event_conversion.py +++ b/src/functions_framework/event_conversion.py @@ -317,7 +317,7 @@ def marshal_background_event_data(request): "data": { "@type": _PUBSUB_MESSAGE_TYPE, "data": request_data["message"]["data"], - "attributes": request_data["message"]["attributes"], + "attributes": request_data["message"].get("attributes", {}), }, } except (AttributeError, KeyError, TypeError): diff --git a/tests/test_convert.py b/tests/test_convert.py index 0d41d5ed..592681e7 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -100,6 +100,14 @@ def raw_pubsub_request(): } +@pytest.fixture +def raw_pubsub_request_noattributes(): + return { + "subscription": "projects/sample-project/subscriptions/gcf-test-sub", + "message": {"data": "eyJmb28iOiJiYXIifQ==", "messageId": "1215011316659232"}, + } + + @pytest.fixture def marshalled_pubsub_request(): return { @@ -121,6 +129,27 @@ def marshalled_pubsub_request(): } +@pytest.fixture +def marshalled_pubsub_request_noattr(): + return { + "data": { + "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", + "data": "eyJmb28iOiJiYXIifQ==", + "attributes": {}, + }, + "context": { + "eventId": "1215011316659232", + "eventType": "google.pubsub.topic.publish", + "resource": { + "name": "projects/sample-project/topics/gcf-test", + "service": "pubsub.googleapis.com", + "type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", + }, + "timestamp": "2021-04-17T07:21:18.249Z", + }, + } + + @pytest.fixture def raw_pubsub_cloud_event_output(marshalled_pubsub_request): event = PUBSUB_CLOUD_EVENT.copy() @@ -343,6 +372,24 @@ def test_marshal_background_event_data_without_topic_in_path( assert payload == marshalled_pubsub_request +def test_marshal_background_event_data_without_topic_in_path_no_attr( + raw_pubsub_request_noattributes, marshalled_pubsub_request_noattr +): + req = flask.Request.from_values( + json=raw_pubsub_request_noattributes, path="/myfunc/" + ) + payload = event_conversion.marshal_background_event_data(req) + + # Remove timestamps as they get generates on the fly + del marshalled_pubsub_request_noattr["context"]["timestamp"] + del payload["context"]["timestamp"] + + # Resource name is set to empty string when it cannot be parsed from the request path + marshalled_pubsub_request_noattr["context"]["resource"]["name"] = "" + + assert payload == marshalled_pubsub_request_noattr + + def test_marshal_background_event_data_with_topic_path( raw_pubsub_request, marshalled_pubsub_request ): From b4ed66673b3dd762c371f755c493a381c8241b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Karpe?= <28049739+MichaelKarpe@users.noreply.github.com> Date: Wed, 18 May 2022 23:00:15 +0200 Subject: [PATCH 09/11] feat: allow for watchdog>=2.0.0 (#186) `mkdocs>=1.2.2` [depends on](https://github.com/mkdocs/mkdocs/blob/cdf8a26cafa6af6cc78a45766dfec235bd7286cc/setup.py#L69) `watchdog>=2.0` and the restriction of watchdog dependency for functions-framework-python done in #101 seems to be due [an issue related to watchdog built distributions](https://github.com/gorakhargosh/watchdog/issues/689#issuecomment-748241552) (as explained by @di) which is [now fixed](https://github.com/gorakhargosh/watchdog/pull/807). I propose to allow for `watchdog>=2.0.0` so that a project can use both `mkdocs>=1.2.2` (or `watchdog>=2.0.0`) and `functions-framework-python>=3.0.0`. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 18727fd6..e685a3ec 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ install_requires=[ "flask>=1.0,<3.0", "click>=7.0,<9.0", - "watchdog>=1.0.0,<2.0.0", + "watchdog>=1.0.0", "gunicorn>=19.2.0,<21.0; platform_system!='Windows'", "cloudevents>=1.2.0,<2.0.0", ], From b7055ed838523cda71bf71bc0149f33271e60ebc Mon Sep 17 00:00:00 2001 From: Grant Timmerman <744973+grant@users.noreply.github.com> Date: Thu, 9 Jun 2022 14:06:13 -0400 Subject: [PATCH 10/11] feat: Add more details to MissingTargetException error (#189) * feat: more detailed function target error message Signed-off-by: Grant Timmerman <744973+grant@users.noreply.github.com> * feat: more detailed function target error message (2) Signed-off-by: Grant Timmerman <744973+grant@users.noreply.github.com> * ci: fix tests Signed-off-by: Grant Timmerman <744973+grant@users.noreply.github.com> * ci: fix ci Signed-off-by: Grant Timmerman <744973+grant@users.noreply.github.com> --- src/functions_framework/_function_registry.py | 11 ++++++++--- tests/test_functions.py | 7 ++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/functions_framework/_function_registry.py b/src/functions_framework/_function_registry.py index cedb7e15..fdcf383f 100644 --- a/src/functions_framework/_function_registry.py +++ b/src/functions_framework/_function_registry.py @@ -38,16 +38,21 @@ def get_user_function(source, source_module, target): """Returns user function, raises exception for invalid function.""" # Extract the target function from the source file if not hasattr(source_module, target): + non_target_functions = ", ".join( + "'{attr}'".format(attr=attr) + for attr in dir(source_module) + if isinstance(getattr(source_module, attr), types.FunctionType) + ) raise MissingTargetException( - "File {source} is expected to contain a function named {target}".format( - source=source, target=target + "File {source} is expected to contain a function named '{target}'. Found: {non_target_functions} instead".format( + source=source, target=target, non_target_functions=non_target_functions ) ) function = getattr(source_module, target) # Check that it is a function if not isinstance(function, types.FunctionType): raise InvalidTargetTypeException( - "The function defined in file {source} as {target} needs to be of " + "The function defined in file {source} as '{target}' needs to be of " "type function. Got: invalid type {target_type}".format( source=source, target=target, target_type=type(function) ) diff --git a/tests/test_functions.py b/tests/test_functions.py index c343205f..c26cb625 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -275,7 +275,8 @@ def test_invalid_function_definition_multiple_entry_points(): create_app(target, source, "event") assert re.match( - "File .* is expected to contain a function named function", str(excinfo.value) + "File .* is expected to contain a function named 'function'. Found: 'fun', 'myFunctionBar', 'myFunctionFoo' instead", + str(excinfo.value), ) @@ -287,7 +288,7 @@ def test_invalid_function_definition_multiple_entry_points_invalid_function(): create_app(target, source, "event") assert re.match( - "File .* is expected to contain a function named invalidFunction", + "File .* is expected to contain a function named 'invalidFunction'. Found: 'fun', 'myFunctionBar', 'myFunctionFoo' instead", str(excinfo.value), ) @@ -300,7 +301,7 @@ def test_invalid_function_definition_multiple_entry_points_not_a_function(): create_app(target, source, "event") assert re.match( - "The function defined in file .* as notAFunction needs to be of type " + "The function defined in file .* as 'notAFunction' needs to be of type " "function. Got: .*", str(excinfo.value), ) From 9af6591465ce59516c5a3058906c0685a0dcea01 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 9 Jun 2022 14:13:59 -0400 Subject: [PATCH 11/11] chore(master): release 3.1.0 (#183) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 21 +++++++++++++++++++++ setup.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfbbe99d..e03a9b4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.1.0](https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v3.0.0...v3.1.0) (2022-06-09) + + +### Features + +* Add more details to MissingTargetException error ([#189](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/189)) ([b7055ed](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/b7055ed838523cda71bf71bc0149f33271e60ebc)) +* allow for watchdog>=2.0.0 ([#186](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/186)) ([b4ed666](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/b4ed66673b3dd762c371f755c493a381c8241b50)) + + +### Bug Fixes + +* Add functools.wraps decorator ([#179](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/179)) ([f2285f9](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/f2285f96072d7ab8f00ada0d5f8c075e2b4ad364)) +* Change gunicorn request line limit to unlimited ([#173](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/173)) ([6f4a360](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/6f4a3608634debe0833d0ef5cd769050b5fecb01)) +* for issue [#170](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/170) gracefully handle pubsub messages without attributes in them ([#187](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/187)) ([a820fd4](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/a820fd4cdb4bef6cffe0ef68a2d03af922f13d7e)) +* Support relative imports for submodules ([#169](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/169)) ([9046388](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/9046388fe8c32e897b83315863ee57ccf7d0e8df)) + + +### Documentation + +* update README to use declarative function signatures ([#171](https://github.com/GoogleCloudPlatform/functions-framework-python/issues/171)) ([efb0e84](https://github.com/GoogleCloudPlatform/functions-framework-python/commit/efb0e84d3f8ada6ac305b216baa6632570c38495)) + ## [Unreleased] ## [3.0.0] - 2021-11-10 diff --git a/setup.py b/setup.py index e685a3ec..ed14b251 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="functions-framework", - version="3.0.0", + version="3.1.0", description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.", long_description=long_description, long_description_content_type="text/markdown",