Skip to content

Commit 99f8972

Browse files
committed
Recover in more cases when a socket cannot be created.
Also: - Clarify some documentation. Use sphinx argument documentation style. - Fix some typos. - Remove a few internal comments marking code sections. - Clarify an error message. - Internally, catch exceptions instead of passing them back. - Change one exception. - Update to pylint 3.1.0 so pre-commit can run under Python 3.12
1 parent 2c79732 commit 99f8972

File tree

2 files changed

+52
-65
lines changed

2 files changed

+52
-65
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ repos:
2323
- id: end-of-file-fixer
2424
- id: trailing-whitespace
2525
- repo: https://github.com/pycqa/pylint
26-
rev: v2.17.4
26+
rev: v3.1.0
2727
hooks:
2828
- id: pylint
2929
name: pylint (library code)

adafruit_connection_manager.py

Lines changed: 51 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
2222
"""
2323

24-
# imports
25-
2624
__version__ = "0.0.0+auto.0"
2725
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ConnectionManager.git"
2826

@@ -31,9 +29,6 @@
3129

3230
WIZNET5K_SSL_SUPPORT_VERSION = (9, 1)
3331

34-
# typing
35-
36-
3732
if not sys.implementation.name == "circuitpython":
3833
from typing import List, Optional, Tuple
3934

@@ -46,9 +41,6 @@
4641
)
4742

4843

49-
# ssl and pool helpers
50-
51-
5244
class _FakeSSLSocket:
5345
def __init__(self, socket: CircuitPythonSocketType, tls_mode: int) -> None:
5446
self._socket = socket
@@ -189,11 +181,8 @@ def get_radio_ssl_context(radio):
189181
return _global_ssl_contexts[_get_radio_hash_key(radio)]
190182

191183

192-
# main class
193-
194-
195184
class ConnectionManager:
196-
"""A library for managing sockets accross libraries."""
185+
"""A library for managing sockets across multiple hardware platforms and libraries."""
197186

198187
def __init__(
199188
self,
@@ -224,23 +213,24 @@ def _get_connected_socket( # pylint: disable=too-many-arguments
224213
is_ssl: bool,
225214
ssl_context: Optional[SSLContextType] = None,
226215
):
227-
try:
228-
socket = self._socket_pool.socket(addr_info[0], addr_info[1])
229-
except (OSError, RuntimeError) as exc:
230-
return exc
216+
217+
socket = self._socket_pool.socket(addr_info[0], addr_info[1])
231218

232219
if is_ssl:
233220
socket = ssl_context.wrap_socket(socket, server_hostname=host)
234221
connect_host = host
235222
else:
236223
connect_host = addr_info[-1][0]
237-
socket.settimeout(timeout) # socket read timeout
224+
225+
# Set socket read and connect timeout.
226+
socket.settimeout(timeout)
238227

239228
try:
240229
socket.connect((connect_host, port))
241-
except (MemoryError, OSError) as exc:
230+
except (MemoryError, OSError):
231+
# If any connect problems, clean up and re-raise the problem exception.
242232
socket.close()
243-
return exc
233+
raise
244234

245235
return socket
246236

@@ -269,82 +259,82 @@ def close_socket(self, socket: SocketType) -> None:
269259
self._available_sockets.remove(socket)
270260

271261
def free_socket(self, socket: SocketType) -> None:
272-
"""Mark a managed socket as available so it can be reused."""
262+
"""Mark a managed socket as available so it can be reused. The socket is not closed."""
273263
if socket not in self._managed_socket_by_key.values():
274264
raise RuntimeError("Socket not managed")
275265
self._available_sockets.add(socket)
276266

267+
def _register_connected_socket(self, key, socket):
268+
self._key_by_managed_socket[socket] = key
269+
self._managed_socket_by_key[key] = socket
270+
271+
# pylint: disable=too-many-arguments
277272
def get_socket(
278273
self,
279274
host: str,
280275
port: int,
281276
proto: str,
282277
session_id: Optional[str] = None,
283278
*,
284-
timeout: float = 1,
279+
timeout: float = 1.0,
285280
is_ssl: bool = False,
286281
ssl_context: Optional[SSLContextType] = None,
287282
) -> CircuitPythonSocketType:
288283
"""
289-
Get a new socket and connect.
290-
291-
- **host** *(str)* – The host you are want to connect to: "www.adaftuit.com"
292-
- **port** *(int)* – The port you want to connect to: 80
293-
- **proto** *(str)* – The protocal you want to use: "http:"
294-
- **session_id** *(Optional[str])* – A unique Session ID, when wanting to have multiple open
295-
connections to the same host
296-
- **timeout** *(float)* – Time timeout used for connecting
297-
- **is_ssl** *(bool)* – If the connection is to be over SSL (auto set when proto is
298-
"https:")
299-
- **ssl_context** *(Optional[SSLContextType])* – The SSL context to use when making SSL
300-
requests
284+
Get a new socket and connect to the given host.
285+
286+
:param str host: host to connect to, such as ``"www.example.org"``
287+
:param int port: port to use for connection, such as ``80`` or ``443``
288+
:param str proto: connection protocol: ``"http:"``, ``"https:"``, etc.
289+
:param Optional[str]: unique session ID,
290+
used for multiple simultaneous connections to the same host
291+
:param float timeout: how long to wait to connect
292+
:param bool is_ssl: ``True`` If the connection is to be over SSL;
293+
automatically set when ``proto`` is ``"https:"`
294+
:param Optional[SSLContextType]: SSL context to use when making SSL requests
301295
"""
302296
if session_id:
303297
session_id = str(session_id)
304298
key = (host, port, proto, session_id)
299+
300+
# Do we have already have a socket available for the requested connection?
305301
if key in self._managed_socket_by_key:
306302
socket = self._managed_socket_by_key[key]
307303
if socket in self._available_sockets:
308304
self._available_sockets.remove(socket)
309305
return socket
310306

311-
raise RuntimeError(f"Socket already connected to {proto}//{host}:{port}")
307+
raise RuntimeError(
308+
f"An existing socket is already connected to {proto}//{host}:{port}"
309+
)
312310

313311
if proto == "https:":
314312
is_ssl = True
315313
if is_ssl and not ssl_context:
316-
raise AttributeError(
317-
"ssl_context must be set before using adafruit_requests for https"
318-
)
314+
raise ValueError("ssl_context must be provided if using ssl")
319315

320316
addr_info = self._socket_pool.getaddrinfo(
321317
host, port, 0, self._socket_pool.SOCK_STREAM
322318
)[0]
323319

324-
first_exception = None
325-
result = self._get_connected_socket(
326-
addr_info, host, port, timeout, is_ssl, ssl_context
327-
)
328-
if isinstance(result, Exception):
329-
# Got an error, if there are any available sockets, free them and try again
320+
try:
321+
socket = self._get_connected_socket(
322+
addr_info, host, port, timeout, is_ssl, ssl_context
323+
)
324+
self._register_connected_socket(key, socket)
325+
return socket
326+
except (MemoryError, OSError, RuntimeError):
327+
# Could not get a new socket (or two, if SSL).
328+
# If there are any available sockets, free them all and try again.
330329
if self.available_socket_count:
331-
first_exception = result
332330
self._free_sockets()
333-
result = self._get_connected_socket(
331+
socket = self._get_connected_socket(
334332
addr_info, host, port, timeout, is_ssl, ssl_context
335333
)
336-
if isinstance(result, Exception):
337-
last_result = f", first error: {first_exception}" if first_exception else ""
338-
raise RuntimeError(
339-
f"Error connecting socket: {result}{last_result}"
340-
) from result
341-
342-
self._key_by_managed_socket[result] = key
343-
self._managed_socket_by_key[key] = result
344-
return result
345-
346-
347-
# global helpers
334+
self._register_connected_socket(key, socket)
335+
return socket
336+
# Re-raise exception if no sockets could be freed.
337+
raise
348338

349339

350340
def connection_manager_close_all(
@@ -353,9 +343,9 @@ def connection_manager_close_all(
353343
"""
354344
Close all open sockets for pool, optionally release references.
355345
356-
- **socket_pool** *(Optional[SocketpoolModuleType])* – A specifc SocketPool you want to close
357-
sockets for, leave blank for all SocketPools
358-
- **release_references** *(bool)* – Set to True if you want to also clear stored references to
346+
:param Optional[SocketpoolModuleType] socket_pool:
347+
a specific `SocketPool` whose sockets you want to close; `None`` means all `SocketPool`s
348+
:param bool release_references: ``True`` if you want to also clear stored references to
359349
the SocketPool and SSL contexts
360350
"""
361351
if socket_pool:
@@ -383,10 +373,7 @@ def connection_manager_close_all(
383373

384374
def get_connection_manager(socket_pool: SocketpoolModuleType) -> ConnectionManager:
385375
"""
386-
Get the ConnectionManager singleton for the given pool.
387-
388-
- **socket_pool** *(Optional[SocketpoolModuleType])* – The SocketPool you want the
389-
ConnectionManager for
376+
Get or create the ConnectionManager singleton for the given pool.
390377
"""
391378
if socket_pool not in _global_connection_managers:
392379
_global_connection_managers[socket_pool] = ConnectionManager(socket_pool)

0 commit comments

Comments
 (0)