Skip to content

Commit 248df16

Browse files
Add method to for named route URL generation (#665)
Co-authored-by: GitHub Copilot <198982749+Copilot@users.noreply.github.com>
1 parent bca5449 commit 248df16

10 files changed

Lines changed: 692 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,29 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [2.6.1] - 2026-??-??
8+
## [2.6.1] - 2026-02-22 :cat:
99

1010
- Fix missing escaping in `multipart/form-data` filenames and content-disposition headers.
1111
- Fix [#193](https://github.com/Neoteroi/BlackSheep/issues/193), adding support for [`a2wsgi`](https://github.com/abersheeran/a2wsgi).
1212
- `raw_path` is optional in ASGI specification. If not present, now the `instantiate_request` method automatically obtains it from `path` and sets it in the scope.
1313
- Static files are now served with `Content-Length` header instead of `Transfer-Encoding: chunked` when file size is known. This improves compatibility with `WSGI` servers via `a2wsgi`.
1414
- Automatically run the `Application` start logic if the `__call__` method is called with **http** or **websocket** messages. This is useful when `lifespan` events are not supported, like when using `WSGI`.
1515
- Fix the issue [#396](https://github.com/Neoteroi/BlackSheep/issues/396). Requests for mounted apps are redirected to a directory (ending with '/') only if the request includes a `Sec-Fetch-Mode: navigate`, which is used by modern browsers to inform the server the request is for navigation. This way, mounted apps serving HTML documents containing relative links work properly (their path must end with `/`). Reported by @satori1995.
16+
- Fix the issue [#256](https://github.com/Neoteroi/BlackSheep/issues/256): add support
17+
for configuring names for routes, and for obtaining URLs by route name. Example:
18+
19+
```python
20+
from blacksheep.routing import URLResolver
21+
22+
23+
@app.router.get("/cats/{cat_id}", name="cat-detail")
24+
async def get_cat_detail(cat_id: int) -> Response:
25+
return Response(200)
26+
27+
@app.router.get("/redirect")
28+
async def redirect_handler(url_resolver: URLResolver) -> Response:
29+
return redirect(url_resolver.url_for("cat-detail", cat_id="42"))
30+
```
1631

1732
## [2.6.0] - 2026-02-15 :cupid:
1833

blacksheep/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
from .server.responses import unauthorized as unauthorized
6767
from .server.routing import Route as Route
6868
from .server.routing import RouteException as RouteException
69+
from .server.routing import RouteNotFound as RouteNotFound
6970
from .server.routing import Router as Router
7071
from .server.routing import RoutesRegistry as RoutesRegistry
7172
from .server.routing import connect as connect

blacksheep/server/application.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,7 @@ async def start(self):
751751
if not self.router.fallback:
752752
self.router.fallback = default_fallback
753753

754+
self.register_default_di_types()
754755
self.router.apply_routes()
755756

756757
if self.on_start:
@@ -773,6 +774,14 @@ async def stop(self):
773774
self.started = False
774775
self._started_complete.clear()
775776

777+
def register_default_di_types(self):
778+
"""
779+
Registers default DI types in the Application DI controller.
780+
By default, only the Application object is registered in the DI controller.
781+
"""
782+
if Application not in self._services:
783+
self._services.register(Application, instance=self)
784+
776785
async def _handle_lifespan(self, receive, send) -> None:
777786
message = await receive()
778787
assert message["type"] == "lifespan.startup"
@@ -912,7 +921,9 @@ def handle_mount_path(self, scope, route_match):
912921
# Update root_path per the ASGI spec: the child app must know its mount
913922
# prefix so it can generate correct absolute URLs (analogous to WSGI
914923
# SCRIPT_NAME). root_path = parent root_path + the stripped mount prefix.
915-
mount_prefix = scope["path"][: -len(tail)] if tail != "/" else scope["path"].rstrip("/")
924+
mount_prefix = (
925+
scope["path"][: -len(tail)] if tail != "/" else scope["path"].rstrip("/")
926+
)
916927
scope["root_path"] = scope.get("root_path", "") + mount_prefix
917928

918929
scope["path"] = tail
@@ -952,8 +963,7 @@ async def _handle_redirect_to_mount_root(self, scope, send):
952963
)
953964
await send_asgi_response(response, send)
954965

955-
def _get_request_scope(self, scope):
956-
...
966+
def _get_request_scope(self, scope): ...
957967

958968
async def __call__(self, scope, receive, send):
959969
if scope["type"] == "lifespan":

blacksheep/server/bindings/__init__.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
https://www.neoteroi.dev/blacksheep/binders/
99
"""
1010

11-
import warnings
1211
from abc import abstractmethod
1312
from collections.abc import Iterable as IterableAbc
1413
from functools import partial
@@ -27,10 +26,11 @@
2726
from guardpost import Identity
2827
from rodi import CannotResolveTypeException, ContainerProtocol
2928

30-
from blacksheep import Request
3129
from blacksheep.contents import FormPart
3230
from blacksheep.exceptions import BadRequest
31+
from blacksheep.messages import Request
3332
from blacksheep.server.bindings.converters import class_converters, converters
33+
from blacksheep.server.routing import Router, URLResolver
3434
from blacksheep.server.websocket import WebSocket
3535
from blacksheep.url import URL
3636

@@ -870,3 +870,28 @@ class FilesBinder(Binder):
870870

871871
async def get_value(self, request: Request) -> list[FormPart]:
872872
return await request.files()
873+
874+
875+
class URLResolverBinder(Binder):
876+
"""
877+
Binder that injects a URLResolver into request handlers.
878+
The URLResolver is constructed per-request from the singleton Router and the
879+
current Request, so it correctly reflects the request's base_path for
880+
generating URLs relative to the mount root.
881+
"""
882+
883+
type_alias = URLResolver
884+
885+
def __init__(self, router: Router, implicit: bool = True):
886+
super().__init__(URLResolver, implicit=implicit)
887+
self._router = router
888+
889+
@classmethod
890+
def from_alias(cls, services: ContainerProtocol) -> "URLResolverBinder":
891+
from blacksheep.server import Application
892+
893+
app: Application = services.resolve(Application)
894+
return cls(app.router)
895+
896+
async def get_value(self, request: Request) -> URLResolver:
897+
return URLResolver(self._router, request)

blacksheep/server/files/dynamic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def serve_files_dynamic(
252252
*,
253253
discovery: bool,
254254
cache_time: int,
255-
extensions: Set[str | None],
255+
extensions: Set[str] | None,
256256
root_path: str,
257257
index_document: str | None,
258258
fallback_document: str | None,

0 commit comments

Comments
 (0)