Repository: sanic-org/sanic-ext (python)

Issue:
[Bug] Using add_dependency with lambda causes error.
**Describe the bug**

From your [offical document](https://sanic.dev/en/plugins/sanic-ext/injection.html#basic-implementation):

> Lower level: app.ext.add_dependency(...)
> 
> - `type: Type,`: some unique class that will be the type of the oject
> - `constructor: Optional[Callable[..., Any]], `(OPTIONAL): a function that will return that type

If the constructor is a lambda expression, it will cause an error like:

```
Traceback (most recent call last):
  File "/.../lib/python3.10/site-packages/sanic_ext/extensions/injection/constructor.py", line 58, in __call__
    retval = self.func(**args)
TypeError: xxx.<locals>.<lambda>() missing 1 required positional argument: 'yyy'
......
sanic.exceptions.ServerError: Failure to inject dependencies. Make sure that all dependencies for '<lambda>' have been registered.
```

**Screenshots**
No need.

**To Reproduce**

Create a clean test environment:

```
conda create -n pure_test_env python=3.10
conda activate pure_test_env
pip install sanic==22.12.0 sanic-ext==22.12.0
```

Make a small change in the [official example](https://sanic.dev/en/plugins/sanic-ext/injection.html#injecting-services)

```python
from sanic import Sanic, Request
from sanic.response import text

app = Sanic('test')

class FakeConnection:
    async def execute(self, query: str, **arguments):
        return "result"

@app.before_server_start
async def setup_db(app, _):
    app.ctx.db_conn = FakeConnection()
    app.ext.add_dependency(FakeConnection, lambda request: request.app.ctx.db_conn)
    # app.ext.add_dependency(FakeConnection, get_db)

def get_db(request: Request):
    return request.app.ctx.db_conn


@app.get("/")
async def handler(request, conn: FakeConnection):
    response = await conn.execute("...")
    return text(response)

if __name__ == "__main__":
    app.run("0.0.0.0", 5000)

```

**Expected behavior**
Visit localhost:5000 and it will display "result"

**Environment (please complete the following information):**
 - OS: WSL2 Ubuntu-20.04
 - Browser: chrome
 - Version sanic==22.12.0, sanic-ext==22.12.0

**Additional context**
= =


Pull request:
Force request property for injections
Explicit set of request argument on an injection

Closes #180 

Patch:
diff --git a/sanic_ext/bootstrap.py b/sanic_ext/bootstrap.py
index 48882d5..bf149ca 100644
--- a/sanic_ext/bootstrap.py
+++ b/sanic_ext/bootstrap.py
@@ -137,10 +137,31 @@ class Extend:
         self,
         type: Type,
         constructor: Optional[Callable[..., Any]] = None,
+        request_arg: Optional[str] = None,
     ) -> None:
+        """
+        Add a dependency for injection
+
+        :param type: The type of the dependency
+        :type type: Type
+        :param constructor: A callable that will return an instance to be
+            injected, when ``False`` it will call the type, defaults to None
+        :type constructor: Optional[Callable[..., Any]], optional
+        :param request_arg: Explicitly state which argument in the
+            constructor (if any) should be a ``Request`` object, when set to
+            ``None`` the constructor will be introspected to check the
+            type annotations looking for a request object. You should really
+            only use this if you **MUST** use a lambda explression, it is
+            otherwise better to use a properly type annotated constructor,
+            defaults to None
+        :type request_arg: Optional[str], optional
+        :raises SanicException: _description_
+        """
         if not self._injection_registry:
             raise SanicException("Injection extension not enabled")
-        self._injection_registry.register(type, constructor)
+        self._injection_registry.register(
+            type, constructor, request_arg=request_arg
+        )
 
     def add_constant(self, name: str, value: Any, overwrite: bool = False):
         if not self._constant_registry:
diff --git a/sanic_ext/extensions/injection/constructor.py b/sanic_ext/extensions/injection/constructor.py
index 0a89bce..c3f17b9 100644
--- a/sanic_ext/extensions/injection/constructor.py
+++ b/sanic_ext/extensions/injection/constructor.py
@@ -30,14 +30,13 @@ class Constructor:
     EXEMPT_ANNOTATIONS = (Request,)
 
     def __init__(
-        self,
-        func: Callable[..., Any],
+        self, func: Callable[..., Any], request_arg: Optional[str] = None
     ):
         self.func = func
         self.injections: Dict[str, Tuple[Type, Constructor]] = {}
         self.constants: Dict[str, Any] = {}
         self.pass_kwargs: bool = False
-        self.request_arg: Optional[str] = None
+        self.request_arg = request_arg
 
     def __str__(self) -> str:
         return f"<{self.__class__.__name__}:{self.func.__name__}>"
diff --git a/sanic_ext/extensions/injection/registry.py b/sanic_ext/extensions/injection/registry.py
index a22642f..ec6029c 100644
--- a/sanic_ext/extensions/injection/registry.py
+++ b/sanic_ext/extensions/injection/registry.py
@@ -25,10 +25,13 @@ class InjectionRegistry:
         return self._registry.get(key, default)
 
     def register(
-        self, _type: Type, constructor: Optional[Callable[..., Any]]
+        self,
+        _type: Type,
+        constructor: Optional[Callable[..., Any]],
+        request_arg: Optional[str] = None,
     ) -> None:
         constructor = constructor or _type
-        constructor = Constructor(constructor)
+        constructor = Constructor(constructor, request_arg=request_arg)
         self._registry[_type] = constructor
 
     def finalize(


Test patch:
diff --git a/tests/extensions/injection/test_add_dependency.py b/tests/extensions/injection/test_add_dependency.py
index c0488ef..1507166 100644
--- a/tests/extensions/injection/test_add_dependency.py
+++ b/tests/extensions/injection/test_add_dependency.py
@@ -4,6 +4,7 @@ import asyncio
 from dataclasses import dataclass
 from itertools import count
 from typing import Optional
+from uuid import UUID
 
 import pytest
 from sanic import Request, json, text
@@ -119,6 +120,11 @@ class Delta:
         self.request = request
 
 
+@dataclass
+class Foo:
+    ident: UUID
+
+
 def make_gamma_with_request(beta: Beta, request: Request):
     return Gamma(beta, request)
 
@@ -395,3 +401,15 @@ def test_injection_class_constructors_with_func_and_no_request(app):
 
     _, response = app.test_client.get("/")
     assert response.json == {"is_gamma": True, "has_request": False}
+
+
+def test_injection_of_lambda_properties(app):
+    @app.get("/foo")
+    def handler(request, foo: Foo):
+        return json(request.id == foo.ident and isinstance(foo.ident, UUID))
+
+    app.ext.add_dependency(Foo, lambda request: Foo(request.id), "request")
+
+    _, response = app.test_client.get("/foo")
+
+    assert response.body == b"true"