Skip to content

Commit 49f6985

Browse files
authored
feat: add support for async functions (#364)
* feat: Introduce functions_framework.aio submodule that support async function execution. * Remove httpx. * Update pyproject.toml to include extra async package. * Update test deps. * Improve test coverage. * Make linter happy. * Fix test harness to support py37. * Remove version filter in tox file. * Remove dependency-groups in pyproject.toml for now. * Use py3.8 compatible types. * Fix more incompatibility with python38 * Pin cloudevent sdk to python37 compatible version. * Fix more py37 incompatibility. * fix: Prevent test_aio.py collection errors on Python 3.7 Add pytest_ignore_collect hook to skip test_aio.py entirely on Python 3.7 to prevent ImportError during test collection. The previous approach using only pytest_collection_modifyitems was too late in the process - the error occurred when pytest tried to import the module before skip markers could be applied. Both hooks are marked as safe to remove when Python 3.7 support is dropped. * style: Apply black formatting to conftest.py * fix: Use modern pytest collection_path parameter and return None - Replace deprecated 'path' parameter with 'collection_path' in pytest_ignore_collect - Return None instead of False to let pytest use default behavior - This should fix the issue where pytest was collecting tests from .tox/.pkg/ * fix: Skip tests parametrized with None on Python 3.7 Simplify the check to just skip any test parametrized with None value. On Python 3.7, create_asgi_app is always None due to the conditional import, so this catches all async-related parametrized tests. * fix: Replace asyncio.to_thread with Python 3.8 compatible code Use asyncio.get_event_loop().run_in_executor() instead of asyncio.to_thread() for Python 3.8 compatibility. Added TODO comments to switch back when Python 3.8 support is dropped. * fix: Improve async test detection for Python 3.7 - Use a list of async keywords (async, asgi, aio, starlette) - Check for these keywords in test names, file paths, and parameters - This catches more async-related tests including those with "aio" prefix * fix: Handle Flask vs Starlette redirect behavior differences - Remove unnecessary follow_redirects=True from Starlette TestClient - Make test_http_function_request_url_empty_path aware of framework differences - Starlette TestClient normalizes empty path "" to "/" while Flask preserves it - Test now expects appropriate behavior for each framework * fix: Exclude aio module from coverage on Python 3.7 Add special coverage configuration for Python 3.7 that excludes the aio module since it requires Python 3.8+ due to Starlette dependency. This prevents coverage failures on Python 3.7. * fix: Simplify conftest.py. * fix: Use full environment names for py37 coverage exclusion The tox environment names in GitHub Actions include the OS suffix (e.g., py37-ubuntu-22.04), so we need to match the full names. * fix: Explicitly list each py37 environment for coverage exclusion - List py37-ubuntu-22.04 and py37-macos-13 explicitly - Place py37 settings before general windows-latest setting - This should properly exclude aio module from coverage on Python 3.7 * fix: Add Python 3.7 specific coverage configuration - Create .coveragerc-py37 to exclude aio module from coverage on Python 3.7 - Use --cov-config flag to specify this file for py37 environments only - This prevents the aio module exclusion from affecting Python 3.8+ tests
1 parent 37e0bf7 commit 49f6985

File tree

20 files changed

+1316
-172
lines changed

20 files changed

+1316
-172
lines changed

.coveragerc-py37

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[run]
2+
# Coverage configuration specifically for Python 3.7 environments
3+
# Excludes the aio module which requires Python 3.8+ (Starlette dependency)
4+
# This file is only used by py37-* tox environments
5+
omit =
6+
*/functions_framework/aio/*
7+
*/.tox/*
8+
*/tests/*
9+
*/venv/*
10+
*/.venv/*

conftest.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,53 @@ def isolate_logging():
4242
sys.stderr = sys.__stderr__
4343
logging.shutdown()
4444
reload(logging)
45+
46+
47+
# Safe to remove when we drop Python 3.7 support
48+
def pytest_ignore_collect(collection_path, config):
49+
"""Ignore async test files on Python 3.7 since Starlette requires Python 3.8+"""
50+
if sys.version_info >= (3, 8):
51+
return None
52+
53+
# Skip test_aio.py entirely on Python 3.7
54+
if collection_path.name == "test_aio.py":
55+
return True
56+
57+
return None
58+
59+
60+
# Safe to remove when we drop Python 3.7 support
61+
def pytest_collection_modifyitems(config, items):
62+
"""Skip async-related tests on Python 3.7 since Starlette requires Python 3.8+"""
63+
if sys.version_info >= (3, 8):
64+
return
65+
66+
skip_async = pytest.mark.skip(
67+
reason="Async features require Python 3.8+ (Starlette dependency)"
68+
)
69+
70+
# Keywords that indicate async-related tests
71+
async_keywords = ["async", "asgi", "aio", "starlette"]
72+
73+
for item in items:
74+
skip_test = False
75+
76+
if hasattr(item, "callspec") and hasattr(item.callspec, "params"):
77+
for param_name, param_value in item.callspec.params.items():
78+
# Check if test has fixtures with async-related parameters
79+
if isinstance(param_value, str) and any(
80+
keyword in param_value.lower() for keyword in async_keywords
81+
):
82+
skip_test = True
83+
break
84+
# Skip tests parametrized with None (create_asgi_app on Python 3.7)
85+
if param_value is None:
86+
skip_test = True
87+
break
88+
89+
# Skip tests that explicitly test async functionality
90+
if any(keyword in item.name.lower() for keyword in async_keywords):
91+
skip_test = True
92+
93+
if skip_test:
94+
item.add_marker(skip_async)

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "functions-framework"
33
version = "3.8.3"
44
description = "An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team."
55
readme = "README.md"
6-
requires-python = ">=3.5, <4"
6+
requires-python = ">=3.7, <4"
77
# Once we drop support for Python 3.7 and 3.8, this can become
88
# license = "Apache-2.0"
99
license = { text = "Apache-2.0" }
@@ -29,11 +29,15 @@ dependencies = [
2929
"gunicorn>=22.0.0; platform_system!='Windows'",
3030
"cloudevents>=1.2.0,<=1.11.0", # Must support python 3.7
3131
"Werkzeug>=0.14,<4.0.0",
32+
"httpx>=0.24.1",
3233
]
3334

3435
[project.urls]
3536
Homepage = "https://github.com/googlecloudplatform/functions-framework-python"
3637

38+
[project.optional-dependencies]
39+
async = ["starlette>=0.37.0,<1.0.0; python_version>='3.8'"]
40+
3741
[project.scripts]
3842
ff = "functions_framework._cli:_cli"
3943
functions-framework = "functions_framework._cli:_cli"

setup.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2020 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from io import open
16+
from os import path
17+
18+
from setuptools import find_packages, setup
19+
20+
here = path.abspath(path.dirname(__file__))
21+
22+
# Get the long description from the README file
23+
with open(path.join(here, "README.md"), encoding="utf-8") as f:
24+
long_description = f.read()
25+
26+
setup(
27+
name="functions-framework",
28+
version="3.8.2",
29+
description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.",
30+
long_description=long_description,
31+
long_description_content_type="text/markdown",
32+
url="https://github.com/googlecloudplatform/functions-framework-python",
33+
author="Google LLC",
34+
author_email="[email protected]",
35+
classifiers=[
36+
"Development Status :: 5 - Production/Stable ",
37+
"Intended Audience :: Developers",
38+
"License :: OSI Approved :: Apache Software License",
39+
"Programming Language :: Python :: 3.7",
40+
"Programming Language :: Python :: 3.8",
41+
"Programming Language :: Python :: 3.9",
42+
"Programming Language :: Python :: 3.10",
43+
"Programming Language :: Python :: 3.11",
44+
"Programming Language :: Python :: 3.12",
45+
],
46+
keywords="functions-framework",
47+
packages=find_packages(where="src"),
48+
package_data={"functions_framework": ["py.typed"]},
49+
namespace_packages=["google", "google.cloud"],
50+
package_dir={"": "src"},
51+
python_requires=">=3.5, <4",
52+
install_requires=[
53+
"flask>=1.0,<4.0",
54+
"click>=7.0,<9.0",
55+
"watchdog>=1.0.0",
56+
"gunicorn>=22.0.0; platform_system!='Windows'",
57+
"cloudevents>=1.2.0,<2.0.0",
58+
"Werkzeug>=0.14,<4.0.0",
59+
],
60+
extras_require={
61+
"async": ["starlette>=0.37.0,<1.0.0"],
62+
},
63+
entry_points={
64+
"console_scripts": [
65+
"ff=functions_framework._cli:_cli",
66+
"functions-framework=functions_framework._cli:_cli",
67+
"functions_framework=functions_framework._cli:_cli",
68+
"functions-framework-python=functions_framework._cli:_cli",
69+
"functions_framework_python=functions_framework._cli:_cli",
70+
]
71+
},
72+
)

0 commit comments

Comments
 (0)