Skip to content

Commit 1237b1e

Browse files
Prepare for 2.3.0 (#557)
1 parent 3b9dcf2 commit 1237b1e

23 files changed

+546
-423
lines changed

CHANGELOG.md

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,41 @@ 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.3.0] - 2025-??-??
9-
10-
- Fix [#511](https://github.com/Neoteroi/BlackSheep/issues/511). Add support for
11-
inheriting endpoints from parent controller classes, when subclassing controllers.
12-
An important feature that was missing so far in the web framework. Example:
8+
## [2.3.0] - 2025-05-10 :sun_behind_small_cloud:
9+
10+
> [!IMPORTANT]
11+
>
12+
> This release, like the previous one, includes some breaking changes to the
13+
> public code API of certain classes, hence the bump in version from `2.2.0` to
14+
> `2.3.0`. The breaking changes aim to improve the user experience (UX) when
15+
> using `Controllers` and registering routes. In particular, they address
16+
> issues [#511](https://github.com/Neoteroi/BlackSheep/issues/511) and
17+
> [#540](https://github.com/Neoteroi/BlackSheep/issues/540).The scope of the
18+
> breaking changes is relatively minor, as they affect built-in features that
19+
> are *likely* not commonly modified: removes the `prepare_controllers` and the
20+
> `get_controller_handler_pattern` from the `Application` class, transferring
21+
> them to a dedicated `ControllersManager` class. Additionally, the `Router`
22+
> class has been refactored to work consistently for request handlers defined
23+
> as _functions_ and those defined as _Controllers' methods_.
24+
>
25+
> The _Router_ now allows registering all request handlers without evaluating
26+
> them immediately, postponing duplicate checks, and introduces an
27+
> `apply_routes` method to make routes effective upon application startup.
28+
> This change is necessary to support using the same functions for both
29+
> _functions_ and _methods_, addressing issue [#540](https://github.com/Neoteroi/BlackSheep/issues/540),
30+
> improving UX, and eliminating potential confusion caused by having two
31+
> sets of decorators (`get, post, put, etc.`) that behave differently. While
32+
> the two sets of decorators are still maintained to minimize the impact of
33+
> breaking changes, the framework now supports using them interchangeably.
34+
>
35+
> While breaking changes may cause inconvenience for some users, I believe the
36+
> new features in this release represent a significant step forward.
37+
> Now Controllers support routes inheritance! This is an important feature that
38+
> was missing so far in the web framework.
39+
40+
- Fix [#511](https://github.com/Neoteroi/BlackSheep/issues/511). Add support
41+
for inheriting endpoints from parent controller classes, when subclassing
42+
controllers. Example:
1343

1444
```python
1545
from blacksheep import Application
@@ -29,13 +59,11 @@ class BaseController(Controller):
2959

3060
class ControllerOne(BaseController):
3161
route = "/one"
32-
3362
# /one/hello-world
3463

3564

3665
class ControllerTwo(BaseController):
3766
route = "/two"
38-
3967
# /two/hello-world
4068

4169
@get("/specific-route") # /two/specific-route
@@ -44,13 +72,18 @@ class ControllerTwo(BaseController):
4472
```
4573

4674
- Add a new `@abstract()` decorator that can be applied to controller classes to skip
47-
routes defined on them (only inherited classes will have the routes, prefixed by a
48-
route).
75+
routes defined in them; so that only their subclasses will have the routes
76+
registered, prefixed by their own prefix).
77+
- **BREAKING CHANGE**. Refactor the `Application` code to encapsulate in a
78+
dedicated class functions that prepare controllers' routes.
79+
- **BREAKING CHANGE**. Refactor the `Router` class to handle consistently
80+
request handlers defined using _functions_ and controllers' class _methods_
81+
(refer to the note above for more information).
4982
- Fix [#498](https://github.com/Neoteroi/BlackSheep/issues/498): Buffer reuse
5083
and race condition in `client.IncomingContent.stream()`, by @ohait.
51-
- Fix [#365](https://github.com/Neoteroi/BlackSheep/issues/365), adding support for
52-
Pydantic's `@validate_call` and `@validate_arguments` and other wrappers applied to
53-
functions before they are configured as request handlers.
84+
- Fix [#365](https://github.com/Neoteroi/BlackSheep/issues/365), adding support
85+
for Pydantic's `@validate_call` and `@validate_arguments` and other wrappers
86+
applied to functions before they are configured as request handlers.
5487
Contribution by @aldem, who reported the issue and provided the solution.
5588
- To better support `@validate_call`, configure automatically a default
5689
exception handler for `pydantic.ValidationError` when Pydantic is installed.
@@ -59,10 +92,21 @@ class ControllerTwo(BaseController):
5992
- Fix [#484](https://github.com/Neoteroi/BlackSheep/issues/484). Improve the
6093
implementation of Server-Sent Events (SSE) to support sending data in any
6194
shape, and not only as JSON. Add a `TextServerSentEvent` class to send plain
62-
text to the client.
95+
text to the client (this still escapes new lines!).
6396
- Modify the `is_stopping` function to emit a warning instead of raising a
6497
`RuntimeError` if the env variable `APP_SIGNAL_HANDLER` is not set to a
6598
truthy value.
99+
- Improve the error message of the `RouteDuplicate` class.
100+
- Fix [#38](https://github.com/Neoteroi/BlackSheep-Docs/issues/38) for notations that
101+
are available since Python 3.9 (e.g. `list[str]`, `set[str]`, `tuple[str]`).
102+
- Fix [a regression](https://github.com/Neoteroi/BlackSheep/issues/538#issuecomment-2867564293)
103+
introduced in `2.2.0` that would prevent custom `HTTPException`handlers from
104+
being used when the user configured a catch-all `Exception` handler
105+
(**this practice is not recommended; let the framework handle unhandled exceptions
106+
using `InternalServerError` exception handler**).
107+
- Add a `Conflict` `HTTPException` to `blacksheep.exceptions` for `409`
108+
[response code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/409).
109+
- Improve the test code to make it less verbose.
66110

67111
## [2.2.0] - 2025-04-28 🎉
68112

blacksheep/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55

66
__author__ = "Roberto Prevato <roberto.prevato@gmail.com>"
7-
__version__ = "2.2.0"
7+
__version__ = "2.3.0"
88

99
from .contents import Content as Content
1010
from .contents import FormContent as FormContent

blacksheep/baseapp.pxd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ cdef class BaseApplication:
1414
cdef readonly object logger
1515
cdef public dict exceptions_handlers
1616
cpdef object get_http_exception_handler(self, HTTPException http_exception)
17-
cdef object get_exception_handler(self, Exception exception)
17+
cdef object get_exception_handler(self, Exception exception, type stop_at)
1818
cdef bint is_handled_exception(self, Exception exception)

blacksheep/baseapp.pyx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,21 +127,28 @@ cdef class BaseApplication:
127127
return await self.handle_exception(request, exc)
128128

129129
cpdef object get_http_exception_handler(self, HTTPException http_exception):
130-
handler_by_type = self.get_exception_handler(http_exception)
131-
if handler_by_type:
132-
return handler_by_type
133-
return self.exceptions_handlers.get(http_exception.status, common_http_exception_handler)
130+
# Try getting HTTP exception handler by type first, supporting
131+
# base classes up to a certain point (HTTPException)
132+
handler = self.get_exception_handler(http_exception, stop_at=HTTPException)
133+
if handler:
134+
return handler
135+
# Try getting HTTP exception handler by HTTP error status code
136+
return self.exceptions_handlers.get(
137+
http_exception.status, common_http_exception_handler
138+
)
134139

135140
cdef bint is_handled_exception(self, Exception exception):
136-
for current_class_in_hierarchy in get_class_instance_hierarchy(exception):
137-
if current_class_in_hierarchy in self.exceptions_handlers:
141+
for class_type in get_class_instance_hierarchy(exception):
142+
if class_type in self.exceptions_handlers:
138143
return True
139144
return False
140145

141-
cdef object get_exception_handler(self, Exception exception):
142-
for current_class_in_hierarchy in get_class_instance_hierarchy(exception):
143-
if current_class_in_hierarchy in self.exceptions_handlers:
144-
return self.exceptions_handlers[current_class_in_hierarchy]
146+
cdef object get_exception_handler(self, Exception exception, type stop_at):
147+
for class_type in get_class_instance_hierarchy(exception):
148+
if stop_at is not None and stop_at is class_type:
149+
return None
150+
if class_type in self.exceptions_handlers:
151+
return self.exceptions_handlers[class_type]
145152

146153
return None
147154

@@ -181,7 +188,7 @@ cdef class BaseApplication:
181188
return await self.handle_exception(request, http_exception)
182189

183190
async def handle_exception(self, request, exc):
184-
exception_handler = self.get_exception_handler(exc)
191+
exception_handler = self.get_exception_handler(exc, None)
185192
if exception_handler:
186193
return await self._apply_exception_handler(request, exc, exception_handler)
187194

blacksheep/contents.pyx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,4 +338,5 @@ cdef class TextServerSentEvent(ServerSentEvent):
338338
super().__init__(data, event, id, retry, comment)
339339

340340
cpdef str write_data(self):
341-
return self.data
341+
# Escape \r\n to avoid issues with data containing EOL
342+
return self.data.replace("\r", "\\r").replace("\n", "\\n")

blacksheep/exceptions.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class Forbidden(HTTPException):
2222
def __init__(self, message: str = "Forbidden"):
2323
super().__init__(403, message)
2424

25+
class Conflict(HTTPException):
26+
def __init__(self, message: str = "Conflict"):
27+
super().__init__(409, message)
28+
2529
class BadRequestFormat(BadRequest):
2630
def __init__(self, message: str, inner_exception: object = None):
2731
super().__init__(message)

blacksheep/exceptions.pyx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ cdef class Forbidden(HTTPException):
5252
super().__init__(403, message or "Forbidden")
5353

5454

55+
cdef class Conflict(HTTPException):
56+
57+
def __init__(self, message=None):
58+
super().__init__(409, message or "Conflict")
59+
60+
5561
cdef class RangeNotSatisfiable(HTTPException):
5662

5763
def __init__(self):

blacksheep/server/application.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,14 +459,14 @@ def use_authorization(
459459
return strategy
460460

461461
def exception_handler(
462-
self, exception: Union[int, Type[Exception]]
462+
self, exception_type: Union[int, Type[Exception]]
463463
) -> Callable[..., Any]:
464464
"""
465465
Registers an exception handler function in the application exception handler.
466466
"""
467467

468468
def decorator(f):
469-
self.exceptions_handlers[exception] = f
469+
self.exceptions_handlers[exception_type] = f
470470
return f
471471

472472
return decorator
@@ -660,6 +660,8 @@ async def start(self):
660660
return
661661

662662
self.started = True
663+
self.router.apply_routes()
664+
663665
if self.on_start:
664666
await self.on_start.fire()
665667

blacksheep/server/controllers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,12 +519,12 @@ def prepare_controllers(self, router: Router) -> List[Type]:
519519
controller_types.append(controller_type)
520520

521521
handler.__annotations__["self"] = ControllerParameter[controller_type]
522-
router.add(
523-
route.method,
522+
new_route = router.create_route(
524523
self.get_controller_handler_pattern(controller_type, route),
525524
handler,
526525
controller_type._filters_,
527526
)
527+
router.add_route(route.method, new_route)
528528
return controller_types
529529

530530
def get_controller_handler_pattern(

blacksheep/server/cors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def decorator(fn):
190190
if not policy_object:
191191
raise CORSPolicyNotConfiguredError(policy)
192192

193-
for route in self.router:
193+
for route in self.router.iter_all():
194194
if route.handler is fn:
195195
self._policies_by_route[route] = policy_object
196196
is_match = True

0 commit comments

Comments
 (0)