From 85bff315fb2747bffaa65573985e5eda88b534a8 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 21 Jul 2025 12:04:53 -0700 Subject: [PATCH 1/7] refactor: move async dependencies from optional to direct dependencies Based on dependency analysis, adding starlette, uvicorn, and uvicorn-worker as direct dependencies only increases installation size by ~816KB (3.5%). This minimal impact makes it reasonable to include them directly, simplifying installation for users who need async functionality while maintaining Python 3.8+ compatibility constraints. --- pyproject.toml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b6e0639..9aa34365 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,18 +30,14 @@ dependencies = [ "cloudevents>=1.2.0,<=1.11.0", # Must support python 3.7 "Werkzeug>=0.14,<4.0.0", "httpx>=0.24.1", + "starlette>=0.37.0,<1.0.0; python_version>='3.8'", + "uvicorn>=0.18.0,<1.0.0; python_version>='3.8'", + "uvicorn-worker>=0.2.0,<1.0.0; python_version>='3.8'", ] [project.urls] Homepage = "https://github.com/googlecloudplatform/functions-framework-python" -[project.optional-dependencies] -async = [ - "starlette>=0.37.0,<1.0.0; python_version>='3.8'", - "uvicorn>=0.18.0,<1.0.0; python_version>='3.8'", - "uvicorn-worker>=0.2.0,<1.0.0; python_version>='3.8'" -] - [project.scripts] ff = "functions_framework._cli:_cli" functions-framework = "functions_framework._cli:_cli" From 43d773e3639441799c6e24b9b59b91d36bdc6883 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 21 Jul 2025 12:36:16 -0700 Subject: [PATCH 2/7] refactor: update imports and tests for direct starlette/uvicorn dependencies - Remove async extras from tox.ini since deps are now direct - Update _http/__init__.py to import StarletteApplication directly - Remove try/except ImportError for starlette in aio/__init__.py - Remove error message about installing with [async] extra - Update test_asgi.py to import starlette directly - Remove test_import_error_without_starlette from test_aio.py - Fix test_http.py monkeypatch to work with direct imports --- src/functions_framework/_http/__init__.py | 5 +---- src/functions_framework/aio/__init__.py | 18 ++++++------------ tests/test_aio.py | 21 --------------------- tests/test_asgi.py | 6 +----- tests/test_http.py | 4 ++-- tox.ini | 4 ---- 6 files changed, 10 insertions(+), 48 deletions(-) diff --git a/src/functions_framework/_http/__init__.py b/src/functions_framework/_http/__init__.py index fa2cbc09..7847f98f 100644 --- a/src/functions_framework/_http/__init__.py +++ b/src/functions_framework/_http/__init__.py @@ -14,6 +14,7 @@ from flask import Flask +from functions_framework._http.asgi import StarletteApplication from functions_framework._http.flask import FlaskApplication @@ -35,8 +36,6 @@ def __init__(self, app, debug, **options): self.server_class = FlaskApplication else: # pragma: no cover if self.debug: - from functions_framework._http.asgi import StarletteApplication - self.server_class = StarletteApplication else: try: @@ -44,8 +43,6 @@ def __init__(self, app, debug, **options): self.server_class = UvicornApplication except ImportError as e: - from functions_framework._http.asgi import StarletteApplication - self.server_class = StarletteApplication def run(self, host, port): diff --git a/src/functions_framework/aio/__init__.py b/src/functions_framework/aio/__init__.py index e30b5f99..2ee384fa 100644 --- a/src/functions_framework/aio/__init__.py +++ b/src/functions_framework/aio/__init__.py @@ -36,18 +36,12 @@ MissingSourceException, ) -try: - from starlette.applications import Starlette - from starlette.exceptions import HTTPException - from starlette.middleware import Middleware - from starlette.requests import Request - from starlette.responses import JSONResponse, Response - from starlette.routing import Route -except ImportError: - raise FunctionsFrameworkException( - "Starlette is not installed. Install the framework with the 'async' extra: " - "pip install functions-framework[async]" - ) +from starlette.applications import Starlette +from starlette.exceptions import HTTPException +from starlette.middleware import Middleware +from starlette.requests import Request +from starlette.responses import JSONResponse, Response +from starlette.routing import Route HTTPResponse = Union[ Response, # Functions can return a full Starlette Response object diff --git a/tests/test_aio.py b/tests/test_aio.py index 4f34c279..916f3a08 100644 --- a/tests/test_aio.py +++ b/tests/test_aio.py @@ -35,27 +35,6 @@ TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" -def test_import_error_without_starlette(monkeypatch): - import builtins - - original_import = builtins.__import__ - - def mock_import(name, *args, **kwargs): - if name.startswith("starlette"): - raise ImportError(f"No module named '{name}'") - return original_import(name, *args, **kwargs) - - monkeypatch.setattr(builtins, "__import__", mock_import) - - # Remove the module from sys.modules to force re-import - if "functions_framework.aio" in sys.modules: - del sys.modules["functions_framework.aio"] - - with pytest.raises(exceptions.FunctionsFrameworkException) as excinfo: - import functions_framework.aio - - assert "Starlette is not installed" in str(excinfo.value) - assert "pip install functions-framework[async]" in str(excinfo.value) def test_invalid_function_definition_missing_function_file(): diff --git a/tests/test_asgi.py b/tests/test_asgi.py index cd117bd3..0428d1a6 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -19,11 +19,7 @@ import pytest import functions_framework._http - -try: - from starlette.applications import Starlette -except ImportError: - pass +from starlette.applications import Starlette def test_httpserver_detects_asgi_app(): diff --git a/tests/test_http.py b/tests/test_http.py index df9d4c6c..bac39345 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -154,9 +154,9 @@ def test_httpserver_asgi(monkeypatch, debug, uvicorn_missing, expected): } options = {"a": pretend.stub(), "b": pretend.stub()} - from functions_framework._http import asgi + import functions_framework._http - monkeypatch.setattr(asgi, "StarletteApplication", server_classes["starlette"]) + monkeypatch.setattr(functions_framework._http, "StarletteApplication", server_classes["starlette"]) if uvicorn_missing or platform.system() == "Windows": monkeypatch.setitem(sys.modules, "functions_framework._http.gunicorn", None) diff --git a/tox.ini b/tox.ini index fd3e38a6..cb0873b6 100644 --- a/tox.ini +++ b/tox.ini @@ -29,8 +29,6 @@ deps = pytest-cov pytest-integration pretend -extras = - async setenv = PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=100 # Python 3.7: Use .coveragerc-py37 to exclude aio module from coverage since it requires Python 3.8+ (Starlette dependency) @@ -48,8 +46,6 @@ deps = isort mypy build -extras = - async commands = black --check src tests conftest.py --exclude tests/test_functions/background_load_error/main.py isort -c src tests conftest.py From 2dec7c9def7d2325365f5c1e5aab2c73f91ab224 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 21 Jul 2025 12:48:04 -0700 Subject: [PATCH 3/7] revert: keep dynamic imports for asgi module to avoid loading for Flask apps Reverts the direct import of StarletteApplication to keep it as a dynamic import. This ensures that Flask-only users don't load the asgi module and its starlette/uvicorn dependencies unnecessarily. --- src/functions_framework/_http/__init__.py | 5 ++++- tests/test_http.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/functions_framework/_http/__init__.py b/src/functions_framework/_http/__init__.py index 7847f98f..fa2cbc09 100644 --- a/src/functions_framework/_http/__init__.py +++ b/src/functions_framework/_http/__init__.py @@ -14,7 +14,6 @@ from flask import Flask -from functions_framework._http.asgi import StarletteApplication from functions_framework._http.flask import FlaskApplication @@ -36,6 +35,8 @@ def __init__(self, app, debug, **options): self.server_class = FlaskApplication else: # pragma: no cover if self.debug: + from functions_framework._http.asgi import StarletteApplication + self.server_class = StarletteApplication else: try: @@ -43,6 +44,8 @@ def __init__(self, app, debug, **options): self.server_class = UvicornApplication except ImportError as e: + from functions_framework._http.asgi import StarletteApplication + self.server_class = StarletteApplication def run(self, host, port): diff --git a/tests/test_http.py b/tests/test_http.py index bac39345..df9d4c6c 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -154,9 +154,9 @@ def test_httpserver_asgi(monkeypatch, debug, uvicorn_missing, expected): } options = {"a": pretend.stub(), "b": pretend.stub()} - import functions_framework._http + from functions_framework._http import asgi - monkeypatch.setattr(functions_framework._http, "StarletteApplication", server_classes["starlette"]) + monkeypatch.setattr(asgi, "StarletteApplication", server_classes["starlette"]) if uvicorn_missing or platform.system() == "Windows": monkeypatch.setitem(sys.modules, "functions_framework._http.gunicorn", None) From 90b1ff931aa9ac213173f2926e3ea86f97b791f1 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 21 Jul 2025 19:37:05 -0700 Subject: [PATCH 4/7] style: run formmater --- tests/test_aio.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_aio.py b/tests/test_aio.py index 916f3a08..e7533b1d 100644 --- a/tests/test_aio.py +++ b/tests/test_aio.py @@ -35,8 +35,6 @@ TEST_FUNCTIONS_DIR = pathlib.Path(__file__).resolve().parent / "test_functions" - - def test_invalid_function_definition_missing_function_file(): source = TEST_FUNCTIONS_DIR / "missing_function_file" / "main.py" target = "function" From 774b3341f6624d91f83d737c5cbd975779020af5 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 21 Jul 2025 21:16:42 -0700 Subject: [PATCH 5/7] style: make linter happer --- src/functions_framework/aio/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/functions_framework/aio/__init__.py b/src/functions_framework/aio/__init__.py index 2ee384fa..8e5f9dc7 100644 --- a/src/functions_framework/aio/__init__.py +++ b/src/functions_framework/aio/__init__.py @@ -25,6 +25,12 @@ from cloudevents.http import from_http from cloudevents.http.event import CloudEvent +from starlette.applications import Starlette +from starlette.exceptions import HTTPException +from starlette.middleware import Middleware +from starlette.requests import Request +from starlette.responses import JSONResponse, Response +from starlette.routing import Route from functions_framework import ( _enable_execution_id_logging, @@ -36,13 +42,6 @@ MissingSourceException, ) -from starlette.applications import Starlette -from starlette.exceptions import HTTPException -from starlette.middleware import Middleware -from starlette.requests import Request -from starlette.responses import JSONResponse, Response -from starlette.routing import Route - HTTPResponse = Union[ Response, # Functions can return a full Starlette Response object str, # Str returns are wrapped in Response(result) From e1bd0bbfc03a2ac3594f5cc6e1d6b9db39fcfbd6 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 21 Jul 2025 21:19:57 -0700 Subject: [PATCH 6/7] fix: correct import order in test_asgi.py --- tests/test_asgi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_asgi.py b/tests/test_asgi.py index 0428d1a6..e5b97e60 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -18,9 +18,10 @@ import pretend import pytest -import functions_framework._http from starlette.applications import Starlette +import functions_framework._http + def test_httpserver_detects_asgi_app(): flask_app = flask.Flask("test") From 7e7986a7a31806e1d34d04e54f6eb78b0987371b Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 21 Jul 2025 21:24:28 -0700 Subject: [PATCH 7/7] fix: remove async extras from conformance-asgi workflow Since async dependencies are now direct dependencies, we no longer need to install with [async] extras in the GitHub workflow. --- .github/workflows/conformance-asgi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/conformance-asgi.yml b/.github/workflows/conformance-asgi.yml index a62fcb71..d975ca3a 100644 --- a/.github/workflows/conformance-asgi.yml +++ b/.github/workflows/conformance-asgi.yml @@ -38,8 +38,8 @@ jobs: with: python-version: ${{ matrix.python }} - - name: Install the framework with async extras - run: python -m pip install -e .[async] + - name: Install the framework + run: python -m pip install -e . - name: Setup Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0