Refactor _from server socket#820
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #820 +/- ##
==========================================
+ Coverage 78.32% 78.76% +0.44%
==========================================
Files 41 41
Lines 4788 4875 +87
Branches 547 555 +8
==========================================
+ Hits 3750 3840 +90
+ Misses 900 896 -4
- Partials 138 139 +1 |
1f86831 to
b2f990a
Compare
This is the first in a sequence of refactors for this function. This step moves the initialization of the remote address to a separate helper function
Moves the check on ignorable errors into a separate function.
Puts the wrapping call into its own function.
the try hamdler had multiple statements
The accept() call is now handled in _accept_conn()
Helper function for security marks the socket fd as non-inheritable and sets the timeout.
Using helper functions this function now just has two main steps. First, socket creation. Second, connection configuration.
Added to deal with comment in _setup_conn_addr()
Added as a contrib.rst.
These tests cover code that was previous not covered.
2081bf2 to
ddd2b92
Compare
Update expected SSL error substrings to match the format returned by Python 3.15 on Windows Server 2025 runners. This unblocks CI coverage reports.
Better coverage and refactored some previous ones
ddd2b92 to
57031ff
Compare
avinashkamat48
left a comment
There was a problem hiding this comment.
In _wrap_socket_for_tls, the FatalSSLAlert branch now returns (None, {}) immediately, so the cleanup block below never closes raw_socket for that error path. The old _from_server_socket path simply returned, but with this refactor the helper comment says both TLS failure paths should close the socket. The NoSSLError branch reaches the close block, while FatalSSLAlert leaks the accepted socket unless the adapter already closed it. Could you either close before returning in that branch or move the common cleanup into a finally/single exit path?
| with _cm.suppress(OSError): | ||
| conn.close() | ||
|
|
||
| def _setup_conn_addr(self, conn, s, addr): |
There was a problem hiding this comment.
Let's use full words for variable names (things like i/j/k/s aren't really descriptive, nor are the shortened words). It'd also be a good idea to settle for all the args being keyword-only or positional-only (seems like in this case positional args of different types make up bad DX).
|
|
||
| def _ignore_socket_oserror(self, exc): | ||
| if self.server.stats['Enabled']: | ||
| self.server.stats['Socket Errors'] += 1 |
There was a problem hiding this comment.
This probably belongs outside a function saying "ignore". Though, maybe the helper could be renamed since it doesn't actually ignore anything but checks for expected exceptions.
How about something like def _is_suppressable_exc(exc: OSError, /) -> bool:? OTOH, could you check if specific OSError subclasses aren't being raised? Maybe, we could replace checking the low-level codes with suppressing the corresponding specific exceptions like InterruptedError and similar?
There was a problem hiding this comment.
Agreed, though I prefer the slightly more intuitive _is_ignorable_socket_error() as the name. I added the / but since none of the other functions have type annotations I left those out — maybe that's better done in a separate PR?
|
|
||
| def _from_server_socket(self, server_socket): # noqa: C901 # FIXME | ||
| try: | ||
| s, addr = server_socket.accept() |
There was a problem hiding this comment.
I wonder why can't we wrap this line with a suppression of a single exception instead of handling a bunch of errors that could come from a lot of arbitrary places within the try-block..
There was a problem hiding this comment.
Refactored to handle two different types of exception - _NoConnectionAvailable, ConnectionError. We keep this separation so that transient accept failures (no connection ready) are kept separate from TLS handshake failures.
| conn.remote_port = addr[1] | ||
|
|
||
| def _ignore_socket_oserror(self, exc): | ||
| """Determine if an OSError during socket operations should be ignored.""" |
There was a problem hiding this comment.
"determine" is another clue that the helper probably shouldn't say "ignore".
| ) | ||
| self._send_bad_request_plain_http_error(s) | ||
| s, ssl_env = self._wrap_socket_for_tls(s, addr) | ||
| if not s: |
There was a problem hiding this comment.
Why can't we communicate this through raising a unified exception (ConnectionError? RuntimeError?) instead of relying on turning one of the returned tuple values into a bool for deciding to bail?
There was a problem hiding this comment.
Refactored along those lines
|
|
||
| def _raise_eintr(*args, **kwargs): | ||
| """Raise an interrupt error.""" | ||
| raise OSError(errno.EINTR, 'Interrupted system call') |
There was a problem hiding this comment.
Wouldn't this be https://docs.python.org/3/library/exceptions.html#InterruptedError IRL?
| actual_client = scenario.client_s or fake_socket | ||
|
|
||
| # Mock the server socket to return our scenario-specific data | ||
| fake_server_socket = SimpleNamespace( |
There was a problem hiding this comment.
the first half could probably go into a fixture with the rest spread across two tests since the logic here is getting nested and tests should be straightforward.
There was a problem hiding this comment.
This may have been addressed by subsequent refactoring — the tests have changed significantly since this comment was made. Could you take another look?
There was a problem hiding this comment.
In general, we should prefer e2e tests since having a lot of mocks will make them tightly coupled with the internals + may make them less reliable.
There was a problem hiding this comment.
Agree in principle, although some of these scenarios (TLS handshake failures, transient socket errors) are difficult to trigger reliably in e2e tests without accessing the internals directly.
| @pytest.fixture | ||
| def fake_socket(): | ||
| """Provide a basic mock socket.""" | ||
| @contextlib.contextmanager |
There was a problem hiding this comment.
this could be a fixture instead
| This function had become highly nested and complex, triggering | ||
| linter warnings (C901, WPS505). Decomposed the logic into | ||
| several smaller private methods. | ||
| -- by :user:`julianz-` |
There was a problem hiding this comment.
| -- by :user:`julianz-` | |
| -- by :user:`julianz-` |
The FatalSSLAlert branch returned early, bypassing the raw_socket.close() cleanup block. Both exception paths now fall through to the shared close.
|
@avinashkamat48 Thanks for the catch. I have fixed _wrap_socket_for_tls() so that both FatalSSLAlert and NoSSLError now fall through to the shared close block. |
62a0316 to
9078584
Compare
The new name better reflects that the method returns a bool rather than performing an action.
Instead of returning (None, None, None, None) on failure, raise _NoConnectionAvailable or ConnectionError to communicate failure explicitly.
Rename s to sock and mf to makefile throughout the private socket helpers, and add positional-only / markers consistent with other methods in the class.
InterruptedError is the specific OSError subclass Python raises for EINTR, making the test more realistic.
fileno() is never called on the server socket when accept() raises, so the pipe file descriptor and its fixture were unnecessary.
socket.timeout has been a subclass of OSError since Python 3.3 and a deprecated alias of TimeoutError since Python 3.10.
Add blank line before the attribution line.
2b6804b to
4ab7e86
Compare
Describe the return tuple in plain prose to avoid abbreviations that the spell checker flags.
4ab7e86 to
96aa39f
Compare
for more information, see https://pre-commit.ci
This function in connections.py had become highly nested and complex, triggering linter warnings (C901, WPS505). Decomposed the logic into several smaller private methods.
❓ What kind of change does this PR introduce?
📋 What is the related issue number (starting with
#)Resolves #
❓ What is the current behavior? (You can also link to an open issue here)
❓ What is the new behavior (if this is a feature change)?
None
📋 Other information:
📋 Contribution checklist:
(If you're a first-timer, check out
[this guide on making great pull requests][making a lovely PR])
the changes have been approved
and description in grammatically correct, complete sentences
Will squash on approval