Repository: encode/uvicorn

Issue:
How to run app from app factory?
I try to run the server by command `uvicorn my_packege.app:create_app` and got error: `TypeError: __call__() missing 1 required positional argument: 'send'`. What could be the reason?


<!-- POLAR PLEDGE BADGE START -->
> [!IMPORTANT]
> - We're using [Polar.sh](https://polar.sh/encode) so you can upvote and help fund this issue.
> - We receive the funding once the issue is completed & confirmed by you.
> - Thank you in advance for helping prioritize & fund our backlog.

<a href="https://polar.sh/encode/uvicorn/issues/492">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/encode/uvicorn/issues/492/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/encode/uvicorn/issues/492/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
ping?
We don't document an app factory style anywhere, and it's not supported.
Run an app *instance* instead.

**mypackage.py**

```python
app = create_app()
```

**Then...**

```shell
$ uvicorn my_package:app
```

I'm curious where the intent is coming from here, since Gunicorn doesn't use an "app factory" style either. Is there a particular Python framework / server that you're comparing this too?

(I'm not totally against us having an invocation like that, but it's not expected usage, and it's not obvious that it's required)
> I'm curious where the intent is coming from here, since Gunicorn doesn't use an "app factory" style either. Is there a particular Python framework / server that you're comparing this too?

flask, aiohttp

> (I'm not totally against us having an invocation like that, but it's not expected usage, and it's not obvious that it's required)

Factory need when you need test the app. If you use a global app then you get one app but in unit tests need to create an app for each test. 
Okay, reviewed https://flask.palletsprojects.com/en/1.1.x/tutorial/factory/ and I see where you're coming from.

I'm used to using a twelve-factor style, and making sure that the system environment variables are setup differently for testing.

We could possibly accept an `--app-factory` flag, which changes the main callable from "the app itself" into "a function returning the app".

Alternately you could instantiate the default application using the factory, so...

```python
def create_app(...):   # We can use this to create a different app instance for testing.
   ...

app = create_app()  # The default case.
```
Recently ran into this myself; the main reason my team did not want to use `app = create_app()` is that it would instantiate an instance whenever the module it exists in (e.g. `from cool_web_thing.app import app`) is imported (`import cool_web_thing.app`) and we weren't sure we wanted that to happen.

We got around it by creating a separate module `from cool_web_thing._devel import app` which we only use for testing changes locally (in production we use Gunicorn).

Also, fwiw, Gunicorn does allow using an app-factory / function:

```shell
gunicorn cool_web_thing.app:create_app() -b :5000 -k uvicorn.workers.UvicornWorker --reload
```

Works while:

```shell
uvicorn cool_web_thing.app:create_app() --port 5000 --reload
```

Does not. 
Creating application as global variable in some module is *worst* ever possible option in the world. Mentioned *twelve-factor app* has absolutely nothing to do with such style of programming.
I _think_ a lazy loading pattern ought to address your use case here…?

```python
# cool_web_thing.py
from .factory import create_app

class LazyApp:
    def __init__(self):
        self._app = None

    async def __call__(self, scope, receive, send):
        if self._app is None:
            self._app = create_app()
        await self._app(scope, receive, send)

app = LazyApp()
```

Run as 

```bash
uvicorn cool_web_thing:app
```

as usual, except the _actual_ app will be instantiated on the first request — meaning you're not risking an "instantiate on module import".

Going to close this off for now since I'm not sure we're going to support app factories _just yet_, especially since this seems to be motivated in more advanced use cases (when startup time matters, e.g. very-large-scale apps/projects), and in that case a bit of extra code like the above shouldn't be an issue.
application factories were added to flask due to the need to not having application creation a side effect of importing modules.  It also allows having different versions/configurations of applications existing at the same time, which is great for unit tests.  This pattern extends further, in that _settings_ are typically stored as an object on the application itself, rather than statics in a settings module.  Different settings can thus be created for different purposes, without having to restart the entire process.
I am a bit surprised that this kind of pattern isn't encouraged and supported by starlette / uvicorn, since it evolved as a response to a need.
:arrow_up: I've had some spare time, and I've created a small PR to support this case. I've read the contribution guide, and I feel bad not asking permission to work on this from the maintainers. But at this point the code was already done. :) 

Resolving patch:
diff --git a/uvicorn/config.py b/uvicorn/config.py
--- a/uvicorn/config.py
+++ b/uvicorn/config.py
@@ -155,6 +155,7 @@ def __init__(
         ssl_ca_certs=None,
         ssl_ciphers="TLSv1",
         headers=None,
+        factory=False,
     ):
         self.app = app
         self.host = host
@@ -191,6 +192,7 @@ def __init__(
         self.ssl_ciphers = ssl_ciphers
         self.headers = headers if headers else []  # type: List[str]
         self.encoded_headers = None  # type: List[Tuple[bytes, bytes]]
+        self.factory = factory
 
         self.loaded = False
         self.configure_logging()
@@ -308,6 +310,19 @@ def load(self):
             logger.error("Error loading ASGI app. %s" % exc)
             sys.exit(1)
 
+        if self.factory:
+            try:
+                self.loaded_app = self.loaded_app()
+            except TypeError as exc:
+                logger.error("Error loading ASGI app factory: %s", exc)
+                sys.exit(1)
+        elif not inspect.signature(self.loaded_app).parameters:
+            logger.error(
+                "APP seems to be an application factory. "
+                "Run uvicorn with the --factory flag."
+            )
+            sys.exit(1)
+
         if self.interface == "auto":
             if inspect.isclass(self.loaded_app):
                 use_asgi_3 = hasattr(self.loaded_app, "__await__")
diff --git a/uvicorn/main.py b/uvicorn/main.py
--- a/uvicorn/main.py
+++ b/uvicorn/main.py
@@ -273,6 +273,13 @@ def print_version(ctx, param, value):
     help="Look for APP in the specified directory, by adding this to the PYTHONPATH."
     " Defaults to the current working directory.",
 )
+@click.option(
+    "--factory",
+    is_flag=True,
+    default=False,
+    help="Treat APP as an application factory, i.e. a () -> <ASGI app> callable.",
+    show_default=True,
+)
 def main(
     app,
     host: str,
@@ -310,6 +317,7 @@ def main(
     headers: typing.List[str],
     use_colors: bool,
     app_dir: str,
+    factory: bool,
 ):
     sys.path.insert(0, app_dir)
 
@@ -349,6 +357,7 @@ def main(
         "ssl_ciphers": ssl_ciphers,
         "headers": list([header.split(":", 1) for header in headers]),
         "use_colors": use_colors,
+        "factory": factory,
     }
     run(**kwargs)
 


Test patch:
diff --git a/tests/test_auto_detection.py b/tests/test_auto_detection.py
--- a/tests/test_auto_detection.py
+++ b/tests/test_auto_detection.py
@@ -23,6 +23,10 @@
     websockets = None
 
 
+async def app(scope, receive, send):
+    pass  # pragma: no cover
+
+
 # TODO: Add pypy to our testing matrix, and assert we get the correct classes
 #       dependent on the platform we're running the tests under.
 
@@ -36,7 +40,7 @@ def test_loop_auto():
 
 
 def test_http_auto():
-    config = Config(app=None)
+    config = Config(app=app)
     server_state = ServerState()
     protocol = AutoHTTPProtocol(config=config, server_state=server_state)
     expected_http = "H11Protocol" if httptools is None else "HttpToolsProtocol"
@@ -44,7 +48,7 @@ def test_http_auto():
 
 
 def test_websocket_auto():
-    config = Config(app=None)
+    config = Config(app=app)
     server_state = ServerState()
     protocol = AutoWebSocketsProtocol(config=config, server_state=server_state)
     expected_websockets = "WSProtocol" if websockets is None else "WebSocketProtocol"
diff --git a/tests/test_config.py b/tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -32,11 +32,11 @@ def yaml_logging_config(logging_config):
     return yaml.dump(logging_config)
 
 
-async def asgi_app():
+async def asgi_app(scope, receive, send):
     pass  # pragma: nocover
 
 
-def wsgi_app():
+def wsgi_app(environ, start_response):
     pass  # pragma: nocover
 
 
@@ -71,6 +71,25 @@ def test_app_unimportable():
         config.load()
 
 
+def test_app_factory():
+    def create_app():
+        return asgi_app
+
+    config = Config(app=create_app, factory=True, proxy_headers=False)
+    config.load()
+    assert config.loaded_app is asgi_app
+
+    # Flag missing.
+    config = Config(app=create_app)
+    with pytest.raises(SystemExit):
+        config.load()
+
+    # App not a no-arguments callable.
+    config = Config(app=asgi_app, factory=True)
+    with pytest.raises(SystemExit):
+        config.load()
+
+
 def test_concrete_http_class():
     config = Config(app=asgi_app, http=H11Protocol)
     config.load()