Repository: GoogleCloudPlatform/functions-framework-python (python)

Issue:
functions-framework --debug exits if you save with a syntax error
Steps to reproduce:

* Start things up normally
```
functions-framework \
    --source=cloud_function/main.py \
    --target=hello_get \
    --host=localhost \
    --port=18000 \
    --debug
```
<details><summary>This starts up as expected:</summary>
```
 * Serving Flask app 'hello_get' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://localhost:18000
Press CTRL+C to quit
 * Restarting with watchdog (fsevents)
 * Debugger is active!
 * Debugger PIN: 104-682-644
```
</details>

Make a change that introduces a syntax error, and you get:
```
 * Detected change in '/Users/david/src/opvia/app/scripts_v3/cloud_function/main.py', reloading
 * Restarting with watchdog (fsevents)
Traceback (most recent call last):
  File "/Users/david/src/opvia/app/.venv/bin/functions-framework", line 8, in <module>
    sys.exit(_cli())
  File "/Users/david/src/opvia/app/.venv/lib/python3.10/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/Users/david/src/opvia/app/.venv/lib/python3.10/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/Users/david/src/opvia/app/.venv/lib/python3.10/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/david/src/opvia/app/.venv/lib/python3.10/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/Users/david/src/opvia/app/.venv/lib/python3.10/site-packages/functions_framework/_cli.py", line 37, in _cli
    app = create_app(target, source, signature_type)
  File "/Users/david/src/opvia/app/.venv/lib/python3.10/site-packages/functions_framework/__init__.py", line 289, in create_app
    spec.loader.exec_module(source_module)
  File "<frozen importlib._bootstrap_external>", line 879, in exec_module
  File "<frozen importlib._bootstrap_external>", line 1017, in get_code
  File "<frozen importlib._bootstrap_external>", line 947, in source_to_code
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/Users/david/src/opvia/app/scripts_v3/cloud_function/main.py", line 11
    ///
    ^^
SyntaxError: invalid syntax
```

and the process exits

Potential solution:

Apparently this used to be an issue in Flask, but isn't anymore (see https://stackoverflow.com/a/68445450 ). I did some digging to work out how they fixed it, and I think I have a solution:

I think that functions-framework basically needs to do something like this:

https://github.com/pallets/flask/pull/4718/files#diff-fed8939d1905b99bba605ad91e9ccdf5ede6223e03ccbc3f0121853035051e62R935-R950

in functions_framework/init.py", line 289, in create_app where it does:
```python
    with _app.app_context():
        spec.loader.exec_module(source_module)
```
* catch the error
* check werkzeug.serving.is_running_from_reloader().
  * if true, print the error and create a dummy app that explodes when called
  * else re-raise the error


Pull request description:
fix: don't exit on reload if there is a syntax error
fixes #213 


Now, if you introduce a syntax error, you immediately get:

```
 * Detected change in '/Users/david/src/opvia/app/scripts_v3/cloud_function/main.py', reloading
 * Restarting with watchdog (fsevents)
Traceback (most recent call last):
  File "/Users/david/src/opvia/app/.venv/lib/python3.10/site-packages/functions_framework/__init__.py", line 290, in create_app
    spec.loader.exec_module(source_module)
  File "<frozen importlib._bootstrap_external>", line 879, in exec_module
  File "<frozen importlib._bootstrap_external>", line 1017, in get_code
  File "<frozen importlib._bootstrap_external>", line 947, in source_to_code
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/Users/david/src/opvia/app/scripts_v3/cloud_function/main.py", line 11
    ///
    ^^
SyntaxError: invalid syntax
 * Debugger is active!
 * Debugger PIN: 104-682-644
```

and then if you curl your fuction endpoint, you get the debug html page for the syntax error, and the backtrace gets printed out to stderr again.

If you attempt to *start* the server when there is already a syntax error present, it correctly exits with the error, as before (the only difference being that the backtrace points at the `raise e from None` line rather than the `spec.loader.exec_module(source_module)` line). 

This behaviour matches the upstream `flask run` behaviour now.

Feel free to be brutal about the quality of my python code: the last time I wrote significant production python, it was before python 2.7 was deprecated ;-) 

Patch:
diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py
index 8d47670..ece4f44 100644
--- a/src/functions_framework/__init__.py
+++ b/src/functions_framework/__init__.py
@@ -357,11 +357,30 @@ def create_app(target=None, source=None, signature_type=None):
 
     # Execute the module, within the application context
     with _app.app_context():
-        spec.loader.exec_module(source_module)
+        try:
+            spec.loader.exec_module(source_module)
+            function = _function_registry.get_user_function(
+                source, source_module, target
+            )
+        except Exception as e:
+            if werkzeug.serving.is_running_from_reloader():
+                # When reloading, print out the error immediately, but raise
+                # it later so the debugger or server can handle it.
+                import traceback
+
+                traceback.print_exc()
+                err = e
+
+                def function(*_args, **_kwargs):
+                    raise err from None
+
+            else:
+                # When not reloading, raise the error immediately so the
+                # command fails.
+                raise e from None
 
     # Get the configured function signature type
     signature_type = _function_registry.get_func_signature_type(target, signature_type)
-    function = _function_registry.get_user_function(source, source_module, target)
 
     _configure_app(_app, function, signature_type)
 


Test patch:
diff --git a/tests/test_functions.py b/tests/test_functions.py
index 81860ca..f0bd779 100644
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -323,6 +323,19 @@ def test_invalid_function_definition_function_syntax_error():
     )
 
 
+def test_invalid_function_definition_function_syntax_robustness_with_debug(monkeypatch):
+    monkeypatch.setattr(
+        functions_framework.werkzeug.serving, "is_running_from_reloader", lambda: True
+    )
+    source = TEST_FUNCTIONS_DIR / "background_load_error" / "main.py"
+    target = "function"
+
+    client = create_app(target, source).test_client()
+
+    resp = client.get("/")
+    assert resp.status_code == 500
+
+
 def test_invalid_function_definition_missing_dependency():
     source = TEST_FUNCTIONS_DIR / "background_missing_dependency" / "main.py"
     target = "function"