Skip to content
70 changes: 57 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,41 @@ 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.3.0] - 2025-??-??

- Fix [#511](https://github.com/Neoteroi/BlackSheep/issues/511). Add support for
inheriting endpoints from parent controller classes, when subclassing controllers.
An important feature that was missing so far in the web framework. Example:
## [2.3.0] - 2025-05-10 :sun_behind_small_cloud:

> [!IMPORTANT]
>
> This release, like the previous one, includes some breaking changes to the
> public code API of certain classes, hence the bump in version from `2.2.0` to
> `2.3.0`. The breaking changes aim to improve the user experience (UX) when
> using `Controllers` and registering routes. In particular, they address
> issues [#511](https://github.com/Neoteroi/BlackSheep/issues/511) and
> [#540](https://github.com/Neoteroi/BlackSheep/issues/540).The scope of the
> breaking changes is relatively minor, as they affect built-in features that
> are *likely* not commonly modified: removes the `prepare_controllers` and the
> `get_controller_handler_pattern` from the `Application` class, transferring
> them to a dedicated `ControllersManager` class. Additionally, the `Router`
> class has been refactored to work consistently for request handlers defined
> as _functions_ and those defined as _Controllers' methods_.
>
> The _Router_ now allows registering all request handlers without evaluating
> them immediately, postponing duplicate checks, and introduces an
> `apply_routes` method to make routes effective upon application startup.
> This change is necessary to support using the same functions for both
> _functions_ and _methods_, addressing issue [#540](https://github.com/Neoteroi/BlackSheep/issues/540),
> improving UX, and eliminating potential confusion caused by having two
> sets of decorators (`get, post, put, etc.`) that behave differently. While
> the two sets of decorators are still maintained to minimize the impact of
> breaking changes, the framework now supports using them interchangeably.
>
> While breaking changes may cause inconvenience for some users, I believe the
> new features in this release represent a significant step forward.
> Now Controllers support routes inheritance! This is an important feature that
> was missing so far in the web framework.

- Fix [#511](https://github.com/Neoteroi/BlackSheep/issues/511). Add support
for inheriting endpoints from parent controller classes, when subclassing
controllers. Example:

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

class ControllerOne(BaseController):
route = "/one"

# /one/hello-world


class ControllerTwo(BaseController):
route = "/two"

# /two/hello-world

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

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

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

Expand Down
2 changes: 1 addition & 1 deletion blacksheep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

__author__ = "Roberto Prevato <roberto.prevato@gmail.com>"
__version__ = "2.2.0"
__version__ = "2.3.0"

from .contents import Content as Content
from .contents import FormContent as FormContent
Expand Down
2 changes: 1 addition & 1 deletion blacksheep/baseapp.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ cdef class BaseApplication:
cdef readonly object logger
cdef public dict exceptions_handlers
cpdef object get_http_exception_handler(self, HTTPException http_exception)
cdef object get_exception_handler(self, Exception exception)
cdef object get_exception_handler(self, Exception exception, type stop_at)
cdef bint is_handled_exception(self, Exception exception)
29 changes: 18 additions & 11 deletions blacksheep/baseapp.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -127,21 +127,28 @@ cdef class BaseApplication:
return await self.handle_exception(request, exc)

cpdef object get_http_exception_handler(self, HTTPException http_exception):
handler_by_type = self.get_exception_handler(http_exception)
if handler_by_type:
return handler_by_type
return self.exceptions_handlers.get(http_exception.status, common_http_exception_handler)
# Try getting HTTP exception handler by type first, supporting
# base classes up to a certain point (HTTPException)
handler = self.get_exception_handler(http_exception, stop_at=HTTPException)
if handler:
return handler
# Try getting HTTP exception handler by HTTP error status code
return self.exceptions_handlers.get(
http_exception.status, common_http_exception_handler
)

cdef bint is_handled_exception(self, Exception exception):
for current_class_in_hierarchy in get_class_instance_hierarchy(exception):
if current_class_in_hierarchy in self.exceptions_handlers:
for class_type in get_class_instance_hierarchy(exception):
if class_type in self.exceptions_handlers:
return True
return False

cdef object get_exception_handler(self, Exception exception):
for current_class_in_hierarchy in get_class_instance_hierarchy(exception):
if current_class_in_hierarchy in self.exceptions_handlers:
return self.exceptions_handlers[current_class_in_hierarchy]
cdef object get_exception_handler(self, Exception exception, type stop_at):
for class_type in get_class_instance_hierarchy(exception):
if stop_at is not None and stop_at is class_type:
return None
if class_type in self.exceptions_handlers:
return self.exceptions_handlers[class_type]

return None

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

async def handle_exception(self, request, exc):
exception_handler = self.get_exception_handler(exc)
exception_handler = self.get_exception_handler(exc, None)
if exception_handler:
return await self._apply_exception_handler(request, exc, exception_handler)

Expand Down
3 changes: 2 additions & 1 deletion blacksheep/contents.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,5 @@ cdef class TextServerSentEvent(ServerSentEvent):
super().__init__(data, event, id, retry, comment)

cpdef str write_data(self):
return self.data
# Escape \r\n to avoid issues with data containing EOL
return self.data.replace("\r", "\\r").replace("\n", "\\n")
4 changes: 4 additions & 0 deletions blacksheep/exceptions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class Forbidden(HTTPException):
def __init__(self, message: str = "Forbidden"):
super().__init__(403, message)

class Conflict(HTTPException):
def __init__(self, message: str = "Conflict"):
super().__init__(409, message)

class BadRequestFormat(BadRequest):
def __init__(self, message: str, inner_exception: object = None):
super().__init__(message)
Expand Down
6 changes: 6 additions & 0 deletions blacksheep/exceptions.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ cdef class Forbidden(HTTPException):
super().__init__(403, message or "Forbidden")


cdef class Conflict(HTTPException):

def __init__(self, message=None):
super().__init__(409, message or "Conflict")


cdef class RangeNotSatisfiable(HTTPException):

def __init__(self):
Expand Down
6 changes: 4 additions & 2 deletions blacksheep/server/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,14 +459,14 @@ def use_authorization(
return strategy

def exception_handler(
self, exception: Union[int, Type[Exception]]
self, exception_type: Union[int, Type[Exception]]
) -> Callable[..., Any]:
"""
Registers an exception handler function in the application exception handler.
"""

def decorator(f):
self.exceptions_handlers[exception] = f
self.exceptions_handlers[exception_type] = f
return f

return decorator
Expand Down Expand Up @@ -660,6 +660,8 @@ async def start(self):
return

self.started = True
self.router.apply_routes()

if self.on_start:
await self.on_start.fire()

Expand Down
4 changes: 2 additions & 2 deletions blacksheep/server/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,12 +519,12 @@ def prepare_controllers(self, router: Router) -> List[Type]:
controller_types.append(controller_type)

handler.__annotations__["self"] = ControllerParameter[controller_type]
router.add(
route.method,
new_route = router.create_route(
self.get_controller_handler_pattern(controller_type, route),
handler,
controller_type._filters_,
)
router.add_route(route.method, new_route)
return controller_types

def get_controller_handler_pattern(
Expand Down
2 changes: 1 addition & 1 deletion blacksheep/server/cors.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def decorator(fn):
if not policy_object:
raise CORSPolicyNotConfiguredError(policy)

for route in self.router:
for route in self.router.iter_all():
if route.handler is fn:
self._policies_by_route[route] = policy_object
is_match = True
Expand Down
15 changes: 15 additions & 0 deletions blacksheep/server/normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,21 @@ def __init__(self, parameter_name, route):
list,
set,
tuple,
list[str],
list[int],
list[float],
list[bool],
list[UUID],
tuple[str],
tuple[int],
tuple[float],
tuple[bool],
tuple[UUID],
set[str],
set[int],
set[float],
set[bool],
set[UUID],
List[str],
List[int],
List[float],
Expand Down
Loading
Loading