Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
3595c73
Experimental
May 1, 2026
f369b00
Refactoring
May 1, 2026
d2cbcc0
Cleanups
May 1, 2026
115246e
Cythonize client
May 2, 2026
66684d0
Optimize
May 2, 2026
a8fa411
Merge branch 'master' into wbs_client
May 2, 2026
240e0a6
Simplify
May 2, 2026
ed2f717
Simplify
May 2, 2026
763e36b
Simplify and optimize
May 3, 2026
a9de297
Cleanup
May 3, 2026
7d0d2a9
Refactoring
May 3, 2026
5a47d89
More refactoring
May 3, 2026
9c77256
Cleanup
May 3, 2026
540ce33
Simplify
May 3, 2026
6af773a
Better compatibility with websockets
May 3, 2026
5ac97e3
Fix mypy issues
May 3, 2026
ef60170
Inline AsyncLock
May 3, 2026
d38746c
Simplify
May 3, 2026
4c88a41
Fix mypy
May 3, 2026
832863f
Cleanup
May 3, 2026
060560f
Simplify logic
May 3, 2026
02ac50f
Better cancellation logic for recv and recv_streaming
May 3, 2026
2fc8e23
Better exception safety
May 3, 2026
41b19dc
Better exception safety
May 3, 2026
e00eab8
Various fixes
May 3, 2026
d612c85
Add permessage-deflate support
May 4, 2026
f008c78
Optimizations
May 4, 2026
27c5f2f
Fix mypy
May 4, 2026
caf454f
Optimizations
May 4, 2026
e9a7909
Merge branch 'master' into wbs_client
May 4, 2026
fe03d7f
Fix tests
May 4, 2026
c5690c6
Clean up logic
May 4, 2026
9ef22c6
Optimize
May 4, 2026
448c1a3
Cleanup
May 4, 2026
b40b54a
Test some edge cases
May 4, 2026
d9ecf9b
Add tests for more edge cases
May 4, 2026
ad1bfc9
Fix missing WSUpgradeResponse.body property on success path
May 5, 2026
807807f
Update AGENTS.md
May 5, 2026
493fc46
Expose WSTransport.is_disconnected attribute
May 5, 2026
af503e3
Update AGENTS.md
May 5, 2026
15f5cf0
Optimizations and cleanups
May 5, 2026
2b4eb1b
Better cancellation and disconnect logic
May 5, 2026
b9d9dde
Simplify logic
May 5, 2026
7170205
Simplify logic
May 5, 2026
b6189d7
Cleanup
May 6, 2026
a49336b
Add server implementation
May 6, 2026
d9c8523
Simplify implmentation
May 7, 2026
08135c9
WSTransport.request was missing for the server side.
May 7, 2026
f4178af
Simplify logic
May 7, 2026
8c7a56e
Clenaup
May 7, 2026
e8e1a65
ws_connect can now accept listener_factory that takes WSUpgradeReques…
May 7, 2026
5b52bda
Simplify
May 7, 2026
a8ce4fa
Fix
May 7, 2026
bfdb6cf
Clenaup
May 7, 2026
8252c40
Re-introduce process_request, process_response on the server side
May 7, 2026
67898d8
Cleanups
May 8, 2026
d121fc2
Refactor tests
May 8, 2026
f59da16
Merge branch 'master' into wbs_client
May 8, 2026
e544120
Fix deps
May 8, 2026
6f518cc
Address mypy issues
May 8, 2026
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
50 changes: 39 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,46 @@

Read README.md for the basic understanding of what this project is.

picows - this is the main package
picows - this is the main package.
picows.websockets - reimplements popular websockets library interface on top of picows
tests - Contains tests for picows
examples - Various examples for users on how to use picows + perf_test that could be used to build call-graph with perf

aiofastnet - Contains optimized versions of asyncio create_connection, create_server
I plan to make a separate python project for it, but I'm not there yet. It should be treated
as a separate python project. It will have its own tests, eventually its own description and docs.
The project contains very efficient repimplementation of SelectSocketTransport and SSLProtocol
using Cython and sometimes a pure C code. create_connection, create_server are defined in aiofastnet/api.py
sslproto.pyx - hack python SSLContext to get raw SSL_CTX*, it works with openssl api directly after that.
sslproto_stdlib.pyx - is just for reference, I will delete it soon, but now it's good for comparison between
stdlib ssl and whatever is in sslproto.pyx.

tests - Contains tests for both picows and aiofastnet. Tests for aiofastnet will become a part of a separate project.
## Code style notes
- Max line width is 120
- Do not write `del transport` or similar `del <parameter>` statements inside callbacks just to mark arguments as unused.
Leave unused callback parameters as-is or rename them with a leading underscore if that is clearer.
Using `del` in this situation is confusing and suggests reference-counting or lifetime management concerns.
- Prefer direct composition only when there is a real behavioral boundary.
Do not introduce adapter / holder / deferred-event plumbing just to preserve a conceptual separation.
If extra machinery exists only to work around the separation you introduced, the separation is probably wrong.
- Do not model impossible or non-normal internal states in the mainline code path without a concrete reason.
If an invariant is guaranteed by control flow, write the code around that invariant instead of adding repeated defensive checks.
Every extra "just in case" branch teaches the reader that the state is part of normal behavior.
Add such checks only for real risks like external misuse, concurrency races, partial failure, or invariants that are genuinely hard to guarantee.
If the only reason for the check is uncertainty in the design, fix the design first.
- When simplifying code, finish the simplification across all equivalent branches, not only at the first local site.
If the same conversion, check, or tiny code pattern appears in multiple sibling paths after a refactor, stop and normalize it before considering the work done.
Do not remove one layer of abstraction only to inline the same logic redundantly in several places.
After a refactor, scan for duplicated branch bodies and duplicated type-specific handling introduced by the change.
- `picows.websockets` aims for import-level compatibility with the official `websockets` package on the client side.
We can skip complicated areas such as the full server interface, but simple surface-area compatibility matters.
Type definitions, exception definitions, and other lightweight importable names should exist when upstream exposes them.
People switching from `websockets` to `picows.websockets` should notice as little difference as possible.
- In Cythonized Python modules, avoid `typing.cast(...)` in hot paths.
Cython may compile `cast(...)` into a real runtime global lookup and function call instead of erasing it like a type checker would.
Prefer control-flow narrowing, assertions, or narrowly scoped type-ignore comments when needed.
- If `picows` core exposes an inconsistent runtime shape or behavior that looks like a bug, do not silently normalize around it in wrapper code.
Stop and ask first, or at least clearly call out that it appears to be a core bug instead of assuming it is an intentional quirk.
Wrapper-level workarounds for such inconsistencies should be treated as temporary and explicit, not as the default resolution.
Legitimate intentional quirks can be documented in this file separately once confirmed.
- `WSUpgradeRequest` / `WSUpgradeResponse` expose a mixed bytes/str API and this is public API.
Request `method`, `path`, `version` and response `version` are low-level protocol bytes, while headers are decoded strings and response `status` is `HTTPStatus`.
Do not change this shape casually in core or silently normalize it away in wrappers; treat it as a stable compatibility constraint unless an intentional breaking change is agreed.
- In `picows` core, once a CLOSE frame has been sent, later send-side API calls are effectively no-ops.
This applies to `send_close()` as well as the other send methods.
Also, `disconnect()` and `wait_disconnected()` are safe to call multiple times.
Wrapper code should rely on these idempotency guarantees instead of adding its own state-based suppression around shutdown operations.

## Testing instructions
- Run lint after updating code with:
Expand Down
2 changes: 0 additions & 2 deletions docs/source/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,6 @@ Classes
.. py:attribute:: payload_size
:type: size_t

**Available only from Cython.**

Size of the payload.

.. autoclass:: WSUpgradeRequest
Expand Down
14 changes: 14 additions & 0 deletions examples/echo_client_websockets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import asyncio

from picows import websockets


async def main():
async with websockets.connect("ws://127.0.0.1:9001") as websocket:
await websocket.send("Hello world")
reply = await websocket.recv()
print(f"Echo reply: {reply}")


if __name__ == "__main__":
asyncio.run(main())
2 changes: 1 addition & 1 deletion picows/picows.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ cdef class MemoryBuffer:
cdef class WSFrame:
cdef:
char* payload_ptr
Py_ssize_t payload_size
readonly Py_ssize_t payload_size
readonly Py_ssize_t tail_size
readonly WSMsgType msg_type
readonly uint8_t fin
Expand Down
7 changes: 5 additions & 2 deletions picows/picows.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class _NotImplemented(Exception):
pass


# "unlikely" works only gcc, but still nice to have
# "unlikely" works only for gcc, but still nice to have
# https://github.com/cython/cython/issues/7667
cdef extern from *:
cdef bint unlikely(bint val) noexcept
Expand Down Expand Up @@ -668,6 +668,8 @@ cdef class WSTransport:

if self.close_handshake is None:
self.close_handshake = <WSCloseHandshake>WSCloseHandshake.__new__(WSCloseHandshake)
self.close_handshake.recv = None
self.close_handshake.sent = None
self.close_handshake.recv_then_sent = False

self.close_handshake.sent = <WSCloseInfo>WSCloseInfo.__new__(WSCloseInfo)
Expand Down Expand Up @@ -1648,7 +1650,8 @@ cdef class WSProtocol(WSProtocolBase, asyncio.BufferedProtocol):
self._f_payload_start_pos = self._f_curr_state_start_pos
self._state = WSParserState.READ_PAYLOAD

if self._f_payload_length > self._max_frame_size:
if (self._f_payload_length > self._max_frame_size and
self._f_msg_type not in (WSMsgType.PING, WSMsgType.PONG, WSMsgType.CLOSE)):
raise WSProtocolError(
WSCloseCode.MESSAGE_TOO_BIG,
f"Received frame with payload size exceeding max allowed size, "
Expand Down
100 changes: 100 additions & 0 deletions picows/websockets/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from . import exceptions
from .asyncio.client import connect
from .asyncio.connection import ClientConnection, ServerConnection, process_exception
from .asyncio.router import route
from .asyncio.server import Server, ServerHandshakeConnection, basic_auth, broadcast, serve
from .compat import CloseCode, Request, Response, State
from .exceptions import (
ConcurrencyError,
ConnectionClosed,
ConnectionClosedError,
ConnectionClosedOK,
DuplicateParameter,
InvalidHandshake,
InvalidHeader,
InvalidHeaderFormat,
InvalidHeaderValue,
InvalidMessage,
InvalidOrigin,
InvalidParameterName,
InvalidParameterValue,
InvalidProxy,
InvalidProxyMessage,
InvalidProxyStatus,
InvalidState,
InvalidStatus,
InvalidUpgrade,
InvalidURI,
NegotiationError,
PayloadTooBig,
ProtocolError,
ProxyError,
SecurityError,
WebSocketException,
)
from .typing import (
BytesLike,
Data,
DataLike,
ExtensionName,
ExtensionParameter,
HeadersLike,
LoggerLike,
Origin,
StatusLike,
Subprotocol,
)

__all__ = [
"BytesLike",
"ClientConnection",
"CloseCode",
"Data",
"DataLike",
"Server",
"ServerHandshakeConnection",
"ServerConnection",
"ConcurrencyError",
"ConnectionClosed",
"ConnectionClosedError",
"ConnectionClosedOK",
"DuplicateParameter",
"ExtensionName",
"ExtensionParameter",
"HeadersLike",
"InvalidHandshake",
"InvalidHeader",
"InvalidHeaderFormat",
"InvalidHeaderValue",
"InvalidMessage",
"InvalidOrigin",
"InvalidParameterName",
"InvalidParameterValue",
"InvalidProxy",
"InvalidProxyMessage",
"InvalidProxyStatus",
"InvalidState",
"InvalidStatus",
"InvalidUpgrade",
"InvalidURI",
"LoggerLike",
"NegotiationError",
"Origin",
"PayloadTooBig",
"ProtocolError",
"ProxyError",
"Request",
"Response",
"SecurityError",
"State",
"StatusLike",
"Subprotocol",
"WebSocketException",
"basic_auth",
"broadcast",
"connect",
"exceptions",
"process_exception",
"route",
"serve",
]
18 changes: 18 additions & 0 deletions picows/websockets/asyncio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from .client import connect
from .connection import ClientConnection, ServerConnection, process_exception
from .router import route
from .server import Server, ServerHandshakeConnection, basic_auth, broadcast, serve
from ..compat import State

__all__ = [
"ClientConnection",
"Server",
"ServerHandshakeConnection",
"ServerConnection",
"basic_auth",
"broadcast",
"connect",
"process_exception",
"route",
"serve",
]
Loading
Loading