Repository: Neoteroi/BlackSheep (python)

Issue:
Together use FromJSON and Request, it does not work at v2.0.4
Unhandled exception - "POST /"
Traceback (most recent call last):
  File "blacksheep\\baseapp.pyx", line 81, in blacksheep.baseapp.BaseApplication.handle
TypeError: hello() missing 1 required positional argument: 'p'

There is a main.py， it works very well at v2.0.3.
But now, it does not work, at v2.0.4. 
---------------------------------------------------------------------
```Python
import uvicorn
from blacksheep import Request, Application, Router, FromJSON
from pydantic.dataclasses import dataclass
from pydantic import Field


@dataclass
class Person:
    id: int | None = None
    name: str = Field()


router = Router()


@router.post("/")
async def hello(request: Request, p: Person):
    return f"{request.client_ip}:Hello, {p.name}!"


app = Application(router=Router(sub_routers=[router]))

if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5556)




Pull request:
Fix #466
- Fix #466
- Prepare for release 2.0.5 to resolve the regression

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47bb98f..91d40a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [2.0.5] - 2024-01-12 :pie:
+
+- Fixes [#466](https://github.com/Neoteroi/BlackSheep/issues/466), regression
+  introduced in 2.0.4 when using sub-routers, reported by @ruancan.
+
 ## [2.0.4] - 2023-12-31 :fireworks:
 
 - Adds a `is_disconnected()` method to the `Request` class, similar to the one
diff --git a/blacksheep/__init__.py b/blacksheep/__init__.py
index 4a3a67c..af734c5 100644
--- a/blacksheep/__init__.py
+++ b/blacksheep/__init__.py
@@ -3,7 +3,7 @@ Root module of the framework. This module re-exports the most commonly
 used types to reduce the verbosity of the imports statements.
 """
 __author__ = "Roberto Prevato <roberto.prevato@gmail.com>"
-__version__ = "2.0.4"
+__version__ = "2.0.5"
 
 from .contents import Content as Content
 from .contents import FormContent as FormContent
diff --git a/blacksheep/server/routing.py b/blacksheep/server/routing.py
index 40b594e..aa3aa35 100644
--- a/blacksheep/server/routing.py
+++ b/blacksheep/server/routing.py
@@ -577,6 +577,16 @@ class MultiRouterMixin:
         for router in self._sub_routers:
             yield from router
 
+    def iter_with_methods(self):
+        """
+        Iters through the routes defined in this Router, yielding each route
+        and its HTTP method.
+        """
+        yield from super().__iter__()  # type: ignore
+
+        for router in self._sub_routers:
+            yield from router.iter_with_methods()
+
     def get_match(self, request: Request) -> Optional[RouteMatch]:
         for router in self._sub_routers:
             match = router.get_match(request)


Test patch:
diff --git a/tests/test_application.py b/tests/test_application.py
index 733c87f..8967c89 100644
--- a/tests/test_application.py
+++ b/tests/test_application.py
@@ -4143,3 +4143,39 @@ def test_mounting_apps_using_the_same_router_raises_error():
 
     with pytest.raises(SharedRouterError):
         Application(router=single_router)
+
+
+@pytest.mark.asyncio
+async def test_application_sub_router_normalization():
+    router = Router()
+    app = FakeApplication(router=Router(sub_routers=[router]))
+
+    # https://github.com/Neoteroi/BlackSheep/issues/466
+    @dataclass
+    class Person:
+        id: Optional[int] = None
+        name: str = ""
+
+    @router.post("/")
+    async def hello(request: Request, p: Person):
+        return f"{request.client_ip}:Hello, {p.name}!"
+
+    content = b'{"id": 1, "name": "Charlie Brown"}'
+
+    await app.start()
+    await app(
+        get_example_scope(
+            "POST",
+            "/",
+            [
+                (b"content-length", str(len(content)).encode()),
+                (b"content-type", b"application/json"),
+            ],
+        ),
+        MockReceive([content]),
+        MockSend(),
+    )
+
+    response = app.response
+    assert response is not None
+    assert response.status == 200