Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial typing of the public API #248

Merged
merged 7 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ functions-framework==3.*
Create an `main.py` file with the following contents:

```python
import flask
import functions_framework

@functions_framework.http
def hello(request):
def hello(request: flask.Request) -> flask.typing.ResponseReturnValue:
return "Hello world!"
```
Comment on lines 61 to 68
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to best show this to users:

  • On one hand, I think it would be great if developers don't have to search for a long time the type definitions of the functions
  • On the other hand, I really like these short examples in the README file

🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including these type annotations for the sake of illustration in this developer focused README.md seems like a reasonable doc update, some of the essence of the simplicity of this sample is preserved on the GCP docs in https://cloud.google.com/functions/docs/samples/functions-helloworld-get#functions_helloworld_get-python.


Expand Down Expand Up @@ -98,9 +99,10 @@ Create an `main.py` file with the following contents:

```python
import functions_framework
from cloudevents.http.event import CloudEvent

@functions_framework.cloud_event
def hello_cloud_event(cloud_event):
def hello_cloud_event(cloud_event: CloudEvent) -> None:
print(f"Received event with ID: {cloud_event['id']} and data {cloud_event.data}")
```

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
],
keywords="functions-framework",
packages=find_packages(where="src"),
package_data={"functions_framework": ["py.typed"]},
namespace_packages=["google", "google.cloud"],
package_dir={"": "src"},
python_requires=">=3.5, <4",
Expand Down
9 changes: 6 additions & 3 deletions src/functions_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@
import types

from inspect import signature
from typing import Type
from typing import Callable, Type

import cloudevents.exceptions as cloud_exceptions
import flask
import werkzeug

from cloudevents.http import from_http, is_binary
from cloudevents.http.event import CloudEvent

from functions_framework import _function_registry, _typed_event, event_conversion
from functions_framework.background_event import BackgroundEvent
Expand All @@ -45,6 +46,8 @@

_CLOUDEVENT_MIME_TYPE = "application/cloudevents+json"

CloudEventFunction = Callable[[CloudEvent], None]
HTTPFunction = Callable[[flask.Request], flask.typing.ResponseReturnValue]
Comment on lines +49 to +50
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signature of the HTTP function is a bit verbose :/

Also, if the type checking fails, the error message is ... interesting :)

$ cat main.py
import functions_framework
import flask


@functions_framework.http
def hello(request: flask.Request) -> flask.typing.ResponseReturnValue:
    return False
$ mypy --strict main.py 
main.py:7: error: Incompatible return value type (got "bool", expected "Union[Union[Response, str, bytes, List[Any], Mapping[str, Any], Iterator[str], Iterator[bytes]], Tuple[Union[Response, str, bytes, List[Any], Mapping[str, Any], Iterator[str], Iterator[bytes]], Union[Headers, Mapping[str, Union[str, List[str], Tuple[str, ...]]], Sequence[Tuple[str, Union[str, List[str], Tuple[str, ...]]]]]], Tuple[Union[Response, str, bytes, List[Any], Mapping[str, Any], Iterator[str], Iterator[bytes]], int], Tuple[Union[Response, str, bytes, List[Any], Mapping[str, Any], Iterator[str], Iterator[bytes]], int, Union[Headers, Mapping[str, Union[str, List[str], Tuple[str, ...]]], Sequence[Tuple[str, Union[str, List[str], Tuple[str, ...]]]]]], Callable[[Dict[str, Any], StartResponse], Iterable[bytes]]]")  [return-value]
Found 1 error in 1 file (checked 1 source file)

I reckon it's more a Flask issue, would you have an idea how to make that better?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is return False something that we'd expect to work? Testing with the latest version of ff I think an error is issued when attempting to return a bool.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember exactly why I returned False in this example, I think the idea was just to return a non-valid type to see what the type checker would show in this case.

So basically, if a developer returns a non-valid type from a @functions_framework.http handler, this will be the error show to the developer.


class _LoggingHandler(io.TextIOWrapper):
"""Logging replacement for stdout and stderr in GCF Python 3.7."""
Expand All @@ -59,7 +62,7 @@ def write(self, out):
return self.stderr.write(json.dumps(payload) + "\n")


def cloud_event(func):
def cloud_event(func: CloudEventFunction) -> CloudEventFunction:
"""Decorator that registers cloudevent as user function signature type."""
_function_registry.REGISTRY_MAP[
func.__name__
Expand Down Expand Up @@ -99,7 +102,7 @@ def wrapper(*args, **kwargs):
return _typed


def http(func):
def http(func: HTTPFunction) -> HTTPFunction:
"""Decorator that registers http as user function signature type."""
_function_registry.REGISTRY_MAP[
func.__name__
Expand Down
Empty file.
14 changes: 14 additions & 0 deletions tests/test_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import typing

if typing.TYPE_CHECKING: # pragma: no cover
import flask
import functions_framework
from cloudevents.http.event import CloudEvent

@functions_framework.http
def hello(request: flask.Request) -> flask.typing.ResponseReturnValue:
return "Hello world!"

@functions_framework.cloud_event
def hello_cloud_event(cloud_event: CloudEvent) -> None:
print(f"Received event: id={cloud_event['id']} and data={cloud_event.data}")
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ deps =
black
twine
isort
mypy
commands =
black --check src tests setup.py conftest.py --exclude tests/test_functions/background_load_error/main.py
isort -c src tests setup.py conftest.py
mypy tests/test_typing.py
python setup.py --quiet sdist bdist_wheel
twine check dist/*
Loading