From ac0ec67b770b7a8be5148b26ecb0e580b4bdb795 Mon Sep 17 00:00:00 2001 From: taotieren Date: Thu, 29 Oct 2026 16:54:49 +0800 Subject: [PATCH 1/3] fix: use vars(self) instead of self.__dict__ in cdef classes Cython 3 forbids accessing self.__dict__ from cdef class instances (Message, Request, Response). Switch to vars(self), which is equivalent at runtime because the base Message class carries the instance __dict__. --- blacksheep/messages.pyx | 44 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/blacksheep/messages.pyx b/blacksheep/messages.pyx index 91791ed6..2a8f343a 100644 --- a/blacksheep/messages.pyx +++ b/blacksheep/messages.pyx @@ -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 @@ -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: @@ -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", "") @@ -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: @@ -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): @@ -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): From 6edc50b7c490cf455e0fb038b3b8908b94719a06 Mon Sep 17 00:00:00 2001 From: taotieren Date: Thu, 29 Oct 2026 16:54:49 +0800 Subject: [PATCH 2/3] fix: drop undeclared Union and AsyncIterator annotations blacksheep/contents.pyx used Union and AsyncIterator in type annotations that were never imported, so Cython 3 reports "undeclared name". Drop the annotations since they were pure type hints. --- blacksheep/contents.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blacksheep/contents.pyx b/blacksheep/contents.pyx index 0f8f7052..8eac04bc 100644 --- a/blacksheep/contents.pyx +++ b/blacksheep/contents.pyx @@ -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)) @@ -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. @@ -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 From cc68f0443cf2a35550577a66a2e987ff8fc77ac9 Mon Sep 17 00:00:00 2001 From: taotieren Date: Thu, 29 Oct 2026 16:54:49 +0800 Subject: [PATCH 3/3] docs: document Cython 3 and Python 3.14 build support --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2cc4b95..a053c467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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