Skip to content

Commit dc9f83c

Browse files
authored
Merge pull request adafruit#84 from michalpokusa/9.0.0-compatibility-and-better-typing
9.0.0 compatibility and better typing for sockets
2 parents d8f9a72 + b00f70f commit dc9f83c

File tree

8 files changed

+133
-56
lines changed

8 files changed

+133
-56
lines changed

adafruit_httpserver/interfaces.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,70 @@
88
"""
99

1010
try:
11-
from typing import List, Dict, Union, Any
11+
from typing import List, Tuple, Dict, Union, Any
1212
except ImportError:
1313
pass
1414

1515

16+
class _ISocket: # pylint: disable=missing-function-docstring,no-self-use,unused-argument
17+
"""A class for typing necessary methods for a socket object."""
18+
19+
def accept(self) -> Tuple["_ISocket", Tuple[str, int]]:
20+
...
21+
22+
def bind(self, address: Tuple[str, int]) -> None:
23+
...
24+
25+
def setblocking(self, flag: bool) -> None:
26+
...
27+
28+
def settimeout(self, value: "Union[float, None]") -> None:
29+
...
30+
31+
def setsockopt(self, level: int, optname: int, value: int) -> None:
32+
...
33+
34+
def listen(self, backlog: int) -> None:
35+
...
36+
37+
def send(self, data: bytes) -> int:
38+
...
39+
40+
def recv_into(self, buffer: memoryview, nbytes: int) -> int:
41+
...
42+
43+
def close(self) -> None:
44+
...
45+
46+
47+
class _ISocketPool: # pylint: disable=missing-function-docstring,no-self-use,unused-argument
48+
"""A class to typing necessary methods and properties for a socket pool object."""
49+
50+
AF_INET: int
51+
SO_REUSEADDR: int
52+
SOCK_STREAM: int
53+
SOL_SOCKET: int
54+
55+
def socket( # pylint: disable=redefined-builtin
56+
self,
57+
family: int = ...,
58+
type: int = ...,
59+
proto: int = ...,
60+
) -> _ISocket:
61+
...
62+
63+
def getaddrinfo( # pylint: disable=redefined-builtin,too-many-arguments
64+
self,
65+
host: str,
66+
port: int,
67+
family: int = ...,
68+
type: int = ...,
69+
proto: int = ...,
70+
flags: int = ...,
71+
) -> Tuple[int, int, int, str, Tuple[str, int]]:
72+
...
73+
74+
1675
class _IFieldStorage:
1776
"""Interface with shared methods for QueryParams, FormData and Headers."""
1877

@@ -62,7 +121,7 @@ def __contains__(self, key: str) -> bool:
62121
return key in self._storage
63122

64123
def __repr__(self) -> str:
65-
return f"{self.__class__.__name__}({repr(self._storage)})"
124+
return f"<{self.__class__.__name__} {repr(self._storage)}>"
66125

67126

68127
def _encode_html_entities(value: Union[str, None]) -> Union[str, None]:

adafruit_httpserver/request.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
try:
1111
from typing import List, Dict, Tuple, Union, Any, TYPE_CHECKING
12-
from socket import socket
13-
from socketpool import SocketPool
1412

1513
if TYPE_CHECKING:
1614
from .server import Server
@@ -20,7 +18,7 @@
2018
import json
2119

2220
from .headers import Headers
23-
from .interfaces import _IFieldStorage, _IXSSSafeFieldStorage
21+
from .interfaces import _ISocket, _IFieldStorage, _IXSSSafeFieldStorage
2422
from .methods import POST, PUT, PATCH, DELETE
2523

2624

@@ -127,11 +125,11 @@ def size(self) -> int:
127125

128126
def __repr__(self) -> str:
129127
filename, content_type, size = (
130-
repr(self.filename),
131-
repr(self.content_type),
132-
repr(self.size),
128+
self.filename,
129+
self.content_type,
130+
self.size,
133131
)
134-
return f"{self.__class__.__name__}({filename=}, {content_type=}, {size=})"
132+
return f"<{self.__class__.__name__} {filename=}, {content_type=}, {size=}>"
135133

136134

137135
class Files(_IFieldStorage):
@@ -260,7 +258,9 @@ def get_list(self, field_name: str, *, safe=True) -> List[Union[str, bytes]]:
260258

261259
def __repr__(self) -> str:
262260
class_name = self.__class__.__name__
263-
return f"{class_name}({repr(self._storage)}, files={repr(self.files._storage)})"
261+
return (
262+
f"<{class_name} {repr(self._storage)}, files={repr(self.files._storage)}>"
263+
)
264264

265265

266266
class Request: # pylint: disable=too-many-instance-attributes
@@ -274,7 +274,7 @@ class Request: # pylint: disable=too-many-instance-attributes
274274
Server object that received the request.
275275
"""
276276

277-
connection: Union["SocketPool.Socket", "socket.socket"]
277+
connection: _ISocket
278278
"""
279279
Socket object used to send and receive data on the connection.
280280
"""
@@ -325,7 +325,7 @@ class Request: # pylint: disable=too-many-instance-attributes
325325
def __init__(
326326
self,
327327
server: "Server",
328-
connection: Union["SocketPool.Socket", "socket.socket"],
328+
connection: _ISocket,
329329
client_address: Tuple[str, int],
330330
raw_request: bytes = None,
331331
) -> None:
@@ -481,6 +481,10 @@ def _parse_request_header(
481481

482482
return method, path, query_params, http_version, headers
483483

484+
def __repr__(self) -> str:
485+
path = self.path + (f"?{self.query_params}" if self.query_params else "")
486+
return f'<{self.__class__.__name__} "{self.method} {path}">'
487+
484488

485489
def _debug_unsupported_form_content_type(content_type: str) -> None:
486490
"""Warns when an unsupported form content type is used."""

adafruit_httpserver/response.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
try:
1111
from typing import Optional, Dict, Union, Tuple, Generator, Any
12-
from socket import socket
13-
from socketpool import SocketPool
1412
except ImportError:
1513
pass
1614

@@ -47,6 +45,7 @@
4745
PERMANENT_REDIRECT_308,
4846
)
4947
from .headers import Headers
48+
from .interfaces import _ISocket
5049

5150

5251
class Response: # pylint: disable=too-few-public-methods
@@ -132,7 +131,7 @@ def _send(self) -> None:
132131

133132
def _send_bytes(
134133
self,
135-
conn: Union["SocketPool.Socket", "socket.socket"],
134+
conn: _ISocket,
136135
buffer: Union[bytes, bytearray, memoryview],
137136
):
138137
bytes_sent: int = 0
@@ -217,6 +216,10 @@ def __init__( # pylint: disable=too-many-arguments
217216
)
218217
self._filename = filename + "index.html" if filename.endswith("/") else filename
219218
self._root_path = root_path or self._request.server.root_path
219+
220+
if self._root_path is None:
221+
raise ValueError("root_path must be provided in Server or in FileResponse")
222+
220223
self._full_file_path = self._combine_path(self._root_path, self._filename)
221224
self._content_type = content_type or MIMETypes.get_for_filename(self._filename)
222225
self._file_length = self._get_file_length(self._full_file_path)
@@ -708,7 +711,7 @@ def _read_frame(self):
708711
length -= min(payload_length, length)
709712

710713
if has_mask:
711-
payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
714+
payload = bytes(byte ^ mask[idx % 4] for idx, byte in enumerate(payload))
712715

713716
return opcode, payload
714717

adafruit_httpserver/route.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,11 @@ def matches(
136136
return True, dict(zip(self.parameters_names, url_parameters_values))
137137

138138
def __repr__(self) -> str:
139-
path = repr(self.path)
140-
methods = repr(self.methods)
141-
handler = repr(self.handler)
139+
path = self.path
140+
methods = self.methods
141+
handler = self.handler
142142

143-
return f"Route({path=}, {methods=}, {handler=})"
143+
return f"<Route {path=}, {methods=}, {handler=}>"
144144

145145

146146
def as_route(

adafruit_httpserver/server.py

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
"""
99

1010
try:
11-
from typing import Callable, Protocol, Union, List, Tuple, Dict, Iterable
12-
from socket import socket
13-
from socketpool import SocketPool
11+
from typing import Callable, Union, List, Tuple, Dict, Iterable
1412
except ImportError:
1513
pass
1614

@@ -28,6 +26,7 @@
2826
ServingFilesDisabledError,
2927
)
3028
from .headers import Headers
29+
from .interfaces import _ISocketPool, _ISocket
3130
from .methods import GET, HEAD
3231
from .request import Request
3332
from .response import Response, FileResponse
@@ -54,7 +53,7 @@ class Server: # pylint: disable=too-many-instance-attributes
5453
"""Root directory to serve files from. ``None`` if serving files is disabled."""
5554

5655
def __init__(
57-
self, socket_source: Protocol, root_path: str = None, *, debug: bool = False
56+
self, socket_source: _ISocketPool, root_path: str = None, *, debug: bool = False
5857
) -> None:
5958
"""Create a server, and get it ready to run.
6059
@@ -172,7 +171,7 @@ def _verify_can_start(self, host: str, port: int) -> None:
172171
raise RuntimeError(f"Cannot start server on {host}:{port}") from error
173172

174173
def serve_forever(
175-
self, host: str, port: int = 80, *, poll_interval: float = 0.1
174+
self, host: str = "0.0.0.0", port: int = 5000, *, poll_interval: float = 0.1
176175
) -> None:
177176
"""
178177
Wait for HTTP requests at the given host and port. Does not return.
@@ -195,15 +194,25 @@ def serve_forever(
195194
except Exception: # pylint: disable=broad-except
196195
pass # Ignore exceptions in handler function
197196

198-
def _set_socket_level_to_reuse_address(self) -> None:
199-
"""
200-
Only for CPython, prevents "Address already in use" error when restarting the server.
201-
"""
202-
self._sock.setsockopt(
203-
self._socket_source.SOL_SOCKET, self._socket_source.SO_REUSEADDR, 1
204-
)
197+
@staticmethod
198+
def _create_server_socket(
199+
socket_source: _ISocketPool,
200+
host: str,
201+
port: int,
202+
) -> _ISocket:
203+
sock = socket_source.socket(socket_source.AF_INET, socket_source.SOCK_STREAM)
204+
205+
# TODO: Temporary backwards compatibility, remove after CircuitPython 9.0.0 release
206+
if implementation.version >= (9,) or implementation.name != "circuitpython":
207+
sock.setsockopt(socket_source.SOL_SOCKET, socket_source.SO_REUSEADDR, 1)
208+
209+
sock.bind((host, port))
210+
sock.listen(10)
211+
sock.setblocking(False) # Non-blocking socket
212+
213+
return sock
205214

206-
def start(self, host: str, port: int = 80) -> None:
215+
def start(self, host: str = "0.0.0.0", port: int = 5000) -> None:
207216
"""
208217
Start the HTTP server at the given host and port. Requires calling
209218
``.poll()`` in a while loop to handle incoming requests.
@@ -216,16 +225,7 @@ def start(self, host: str, port: int = 80) -> None:
216225
self.host, self.port = host, port
217226

218227
self.stopped = False
219-
self._sock = self._socket_source.socket(
220-
self._socket_source.AF_INET, self._socket_source.SOCK_STREAM
221-
)
222-
223-
if implementation.name != "circuitpython":
224-
self._set_socket_level_to_reuse_address()
225-
226-
self._sock.bind((host, port))
227-
self._sock.listen(10)
228-
self._sock.setblocking(False) # Non-blocking socket
228+
self._sock = self._create_server_socket(self._socket_source, host, port)
229229

230230
if self.debug:
231231
_debug_started_server(self)
@@ -244,9 +244,7 @@ def stop(self) -> None:
244244
if self.debug:
245245
_debug_stopped_server(self)
246246

247-
def _receive_header_bytes(
248-
self, sock: Union["SocketPool.Socket", "socket.socket"]
249-
) -> bytes:
247+
def _receive_header_bytes(self, sock: _ISocket) -> bytes:
250248
"""Receive bytes until a empty line is received."""
251249
received_bytes = bytes()
252250
while b"\r\n\r\n" not in received_bytes:
@@ -263,7 +261,7 @@ def _receive_header_bytes(
263261

264262
def _receive_body_bytes(
265263
self,
266-
sock: Union["SocketPool.Socket", "socket.socket"],
264+
sock: _ISocket,
267265
received_body_bytes: bytes,
268266
content_length: int,
269267
) -> bytes:
@@ -282,7 +280,7 @@ def _receive_body_bytes(
282280

283281
def _receive_request(
284282
self,
285-
sock: Union["SocketPool.Socket", "socket.socket"],
283+
sock: _ISocket,
286284
client_address: Tuple[str, int],
287285
) -> Request:
288286
"""Receive bytes from socket until the whole request is received."""
@@ -530,6 +528,13 @@ def socket_timeout(self, value: int) -> None:
530528
else:
531529
raise ValueError("Server.socket_timeout must be a positive numeric value.")
532530

531+
def __repr__(self) -> str:
532+
host = self.host
533+
port = self.port
534+
root_path = self.root_path
535+
536+
return f"<Server {host=}, {port=}, {root_path=}>"
537+
533538

534539
def _debug_warning_exposed_files(root_path: str):
535540
"""Warns about exposing all files on the device."""

adafruit_httpserver/status.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@ def __init__(self, code: int, text: str):
2121
self.code = code
2222
self.text = text
2323

24-
def __repr__(self):
25-
return f'Status({self.code}, "{self.text}")'
24+
def __eq__(self, other: "Status"):
25+
return self.code == other.code and self.text == other.text
2626

2727
def __str__(self):
2828
return f"{self.code} {self.text}"
2929

30-
def __eq__(self, other: "Status"):
31-
return self.code == other.code and self.text == other.text
30+
def __repr__(self):
31+
code = self.code
32+
text = self.text
33+
34+
return f'<Status {code}, "{text}">'
3235

3336

3437
SWITCHING_PROTOCOLS_101 = Status(101, "Switching Protocols")

0 commit comments

Comments
 (0)