Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [2.6.5] - Unreleased

- Fix compilation under Cython 3 and Python 3.14: stop accessing `self.__dict__`
directly in `cdef` classes (use `vars(self)` instead), and remove `Union` /
`AsyncIterator` type annotations that Cython 3 cannot resolve.
- Improve header ergonomics: dictionary-style assignment and `Headers.update()`
accept strings or bytes, so `response.headers["X-Example"] = "value"` is
supported while headers remain stored internally as bytes. String names use
Expand Down
6 changes: 3 additions & 3 deletions blacksheep/contents.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ cpdef bytes write_www_form_urlencoded(data: dict | list):

cdef class FormContent(Content):

def __init__(self, data: Union[dict[str, str], list[tuple[str, str]]]):
def __init__(self, data):
super().__init__(b'application/x-www-form-urlencoded', write_www_form_urlencoded(data))


Expand Down Expand Up @@ -520,7 +520,7 @@ cdef class FileBuffer:
self.size = size
self._charset = charset

async def stream(self, chunk_size: int = 131072) -> AsyncIterator[bytes]:
async def stream(self, chunk_size: int = 131072):
"""
Async generator that yields the data in chunks.

Expand Down Expand Up @@ -691,7 +691,7 @@ cdef class MultiPartFormData(StreamedContent):
data_length=-1,
)

async def _generate_multipart_chunks(self) -> AsyncIterator[bytes]:
async def _generate_multipart_chunks(self):
"""Generate multipart/form-data content in chunks."""
# Import the escape function from multipart module
from blacksheep.multipart import _escape_quoted_string
Expand Down
44 changes: 22 additions & 22 deletions blacksheep/messages.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ cdef class Message:
@property
def headers(self):
cdef str key = '_headers'
if key in self.__dict__:
return self.__dict__[key]
self.__dict__[key] = Headers(self._raw_headers)
return self.__dict__[key]
if key in vars(self):
return vars(self)[key]
vars(self)[key] = Headers(self._raw_headers)
return vars(self)[key]

cpdef Message with_content(self, Content content):
self.content = content
Expand Down Expand Up @@ -472,46 +472,46 @@ cdef class Request(Message):

@identity.setter
def identity(self, value):
self.__dict__["_user"] = value
vars(self)["_user"] = value

@property
def user(self):
try:
return self.__dict__["_user"]
return vars(self)["_user"]
except KeyError:
self.__dict__["_user"] = Identity() # no claims, unauthenticated
return self.__dict__["_user"]
vars(self)["_user"] = Identity() # no claims, unauthenticated
return vars(self)["_user"]

@user.setter
def user(self, value):
self.__dict__["_user"] = value
vars(self)["_user"] = value

@property
def scheme(self) -> str:
return self.__dict__.get("scheme") or (self.scope.get("scheme", "") if self.scope else "")
return vars(self).get("scheme") or (self.scope.get("scheme", "") if self.scope else "")

@scheme.setter
def scheme(self, value: str):
# this can be set, for example when handling forward headers
self.__dict__["scheme"] = value
vars(self)["scheme"] = value

@property
def host(self) -> str:
if not self.__dict__.get("host"):
if not vars(self).get("host"):
if self._url is not None and self._url.is_absolute:
self.__dict__["host"] = self._url.host.decode()
vars(self)["host"] = self._url.host.decode()
else:
# default to host header
host_header = self.get_first_header(b'host')
if host_header is None:
raise BadRequest("Missing Host header")
self.__dict__["host"] = host_header.decode()
return self.__dict__["host"]
vars(self)["host"] = host_header.decode()
return vars(self)["host"]

@host.setter
def host(self, value: str) -> None:
# this can be set, for example when handling forward headers
self.__dict__["host"] = value
vars(self)["host"] = value

@property
def path(self) -> str:
Expand All @@ -523,7 +523,7 @@ cdef class Request(Message):
# 2. if a root_path is set in the ASGI scope, use it
# 3. default to empty string otherwise
try:
return self.__dict__["base_path"]
return vars(self)["base_path"]
except KeyError:
try:
return self.scope.get("root_path", "")
Expand All @@ -533,7 +533,7 @@ cdef class Request(Message):
@base_path.setter
def base_path(self, value: str):
# this can be set, for example when handling forward headers
self.__dict__["base_path"] = value
vars(self)["base_path"] = value

@property
def client_ip(self) -> str:
Expand All @@ -544,14 +544,14 @@ cdef class Request(Message):

@property
def original_client_ip(self) -> str:
if "original_client_ip" in self.__dict__:
return self.__dict__["original_client_ip"]
if "original_client_ip" in vars(self):
return vars(self)["original_client_ip"]

return self.client_ip

@original_client_ip.setter
def original_client_ip(self, value: str):
self.__dict__["original_client_ip"] = value
vars(self)["original_client_ip"] = value

@property
def session(self):
Expand Down Expand Up @@ -621,7 +621,7 @@ cdef class Request(Message):
self._raw_query = None
self._url = _url
# unset the cached host
self.__dict__["host"] = None
vars(self)["host"] = None
self.remove_header(b"host")

def __repr__(self):
Expand Down