| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -58,3 +58,11 @@ repos: | |||
| 58 | 58 | hooks: | |
| 59 | 59 | - id: cython-lint | |
| 60 | 60 | - id: double-quote-cython-strings | |
| 61 | + - repo: local | ||
| 62 | + hooks: | ||
| 63 | + - id: check-declaration-order | ||
| 64 | + name: check class declaration order | ||
| 65 | + entry: python scripts/check_declaration_order.py | ||
| 66 | + language: system | ||
| 67 | + pass_filenames: false | ||
| 68 | + files: ^src/zeroconf/.*\.py$ | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -183,6 +183,9 @@ select = [ | |||
| 183 | 183 | "examples/**/*" = [ | |
| 184 | 184 | "T201", # intended | |
| 185 | 185 | ] | |
| 186 | + "scripts/**/*" = [ | ||
| 187 | + "T201", # intended | ||
| 188 | + ] | ||
| 186 | 189 | "setup.py" = ["D100"] | |
| 187 | 190 | "conftest.py" = ["D100"] | |
| 188 | 191 | "docs/conf.py" = ["D100"] | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,50 @@ | |||
| 1 | + """Enforce the repository's class declaration ordering convention.""" | ||
| 2 | + | ||
| 3 | + from __future__ import annotations | ||
| 4 | + | ||
| 5 | + import ast | ||
| 6 | + import sys | ||
| 7 | + from pathlib import Path | ||
| 8 | + | ||
| 9 | + SRC = Path(__file__).resolve().parent.parent / "src" / "zeroconf" | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + def sort_key(name: str) -> tuple[int, int, str]: | ||
| 13 | + is_dunder = name.startswith("__") and name.endswith("__") | ||
| 14 | + is_private = not is_dunder and name.startswith("_") | ||
| 15 | + group = 0 if is_dunder else (2 if is_private else 1) | ||
| 16 | + return (group, 0 if name == "__init__" else 1, name) | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + def check_class(path: Path, cls: ast.ClassDef) -> list[str]: | ||
| 20 | + names = [node.name for node in cls.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))] | ||
| 21 | + expected = sorted(names, key=sort_key) | ||
| 22 | + if names == expected: | ||
| 23 | + return [] | ||
| 24 | + # property setters share the getter's name; a stable sort keeps their | ||
| 25 | + # relative order, so equal-name adjacency is always satisfiable | ||
| 26 | + for actual, want in zip(names, expected, strict=True): | ||
| 27 | + if actual != want: | ||
| 28 | + return [ | ||
| 29 | + f"{path}:{cls.lineno}: class {cls.name}: method order breaks the " | ||
| 30 | + f"convention at {actual!r} (expected {want!r}); required order is " | ||
| 31 | + f"dunders with __init__ first, then public, then private, each " | ||
| 32 | + f"alphabetical" | ||
| 33 | + ] | ||
| 34 | + return [] | ||
| 35 | + | ||
| 36 | + | ||
| 37 | + def main() -> int: | ||
| 38 | + problems: list[str] = [] | ||
| 39 | + for path in sorted(SRC.rglob("*.py")): | ||
| 40 | + tree = ast.parse(path.read_text()) | ||
| 41 | + for node in ast.walk(tree): | ||
| 42 | + if isinstance(node, ast.ClassDef): | ||
| 43 | + problems.extend(check_class(path.relative_to(SRC.parent.parent), node)) | ||
| 44 | + for problem in problems: | ||
| 45 | + print(problem) | ||
| 46 | + return 1 if problems else 0 | ||
| 47 | + | ||
| 48 | + | ||
| 49 | + if __name__ == "__main__": | ||
| 50 | + sys.exit(main()) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -117,68 +117,6 @@ def __init__( | |||
| 117 | 117 | self._cleanup_timer: asyncio.TimerHandle | None = None | |
| 118 | 118 | self._setup_task: asyncio.Task[None] | None = None | |
| 119 | 119 | ||
| 120 | - def setup( | ||
| 121 | - self, | ||
| 122 | - loop: asyncio.AbstractEventLoop, | ||
| 123 | - loop_thread_ready: threading.Event | None, | ||
| 124 | - ) -> None: | ||
| 125 | - """Set up the instance.""" | ||
| 126 | - self.loop = loop | ||
| 127 | - self.running_future = loop.create_future() | ||
| 128 | - self._setup_task = self.loop.create_task(self._async_setup(loop_thread_ready)) | ||
| 129 | - | ||
| 130 | - async def _async_setup(self, loop_thread_ready: threading.Event | None) -> None: | ||
| 131 | - """Set up the instance.""" | ||
| 132 | - self._async_schedule_next_cache_cleanup() | ||
| 133 | - await self._async_create_endpoints() | ||
| 134 | - assert self.running_future is not None | ||
| 135 | - if not self.running_future.done(): | ||
| 136 | - self.running_future.set_result(True) | ||
| 137 | - if loop_thread_ready: | ||
| 138 | - loop_thread_ready.set() | ||
| 139 | - | ||
| 140 | - async def _async_create_endpoints(self) -> None: | ||
| 141 | - """Create endpoints to send and receive.""" | ||
| 142 | - reader_sockets = [] | ||
| 143 | - sender_sockets = [] | ||
| 144 | - if self._listen_socket: | ||
| 145 | - reader_sockets.append(self._listen_socket) | ||
| 146 | - for s in self._respond_sockets: | ||
| 147 | - if s not in reader_sockets: | ||
| 148 | - reader_sockets.append(s) | ||
| 149 | - sender_sockets.append(s) | ||
| 150 | - | ||
| 151 | - for s in reader_sockets: | ||
| 152 | - reader = await self._async_wrap_socket(s, s in sender_sockets) | ||
| 153 | - # _async_wrap_socket registers the transport with no await between | ||
| 154 | - # creating and registering it, and the pending-handle cleanup below | ||
| 155 | - # adds no await either, so a concurrent shutdown always sees ``s`` | ||
| 156 | - # in exactly one place. | ||
| 157 | - if s is self._listen_socket: | ||
| 158 | - # Keep a handle to the shared listen socket so interface | ||
| 159 | - # rescans can add/drop multicast memberships on it. | ||
| 160 | - self._listen_transport = reader | ||
| 161 | - self._listen_socket = None | ||
| 162 | - if s in self._respond_sockets: | ||
| 163 | - self._respond_sockets.remove(s) | ||
| 164 | - | ||
| 165 | - async def _async_wrap_socket(self, sock: socket.socket, is_sender: bool) -> _WrappedTransport: | ||
| 166 | - """Adopt a socket into a transport, register it, and return the reader wrapper.""" | ||
| 167 | - assert self.loop is not None | ||
| 168 | - transport, protocol = await self.loop.create_datagram_endpoint( # type: ignore[type-var] | ||
| 169 | - lambda: AsyncListener(self.zc), # type: ignore[arg-type, return-value] | ||
| 170 | - sock=sock, | ||
| 171 | - ) | ||
| 172 | - datagram_transport = cast(asyncio.DatagramTransport, transport) | ||
| 173 | - reader = make_wrapped_transport(datagram_transport) | ||
| 174 | - # No ``await`` between wrapping and registering so a concurrent | ||
| 175 | - # shutdown always sees the transport in exactly one place. | ||
| 176 | - self.protocols.append(cast(AsyncListener, protocol)) | ||
| 177 | - self.readers.append(reader) | ||
| 178 | - if is_sender: | ||
| 179 | - self.senders.append(make_wrapped_transport(datagram_transport)) | ||
| 180 | - return reader | ||
| 181 | - | ||
| 182 | 120 | async def async_update_interfaces( | |
| 183 | 121 | self, | |
| 184 | 122 | interfaces: InterfacesType, | |
@@ -271,6 +209,32 @@ async def async_update_interfaces( | |||
| 271 | 209 | added = True | |
| 272 | 210 | return added | |
| 273 | 211 | ||
| 212 | + def close(self) -> None: | ||
| 213 | + """Close from sync context. | ||
| 214 | + | ||
| 215 | + While it is not expected during normal operation, | ||
| 216 | + this function may raise EventLoopBlocked if the underlying | ||
| 217 | + call to `_async_close` cannot be completed. | ||
| 218 | + """ | ||
| 219 | + assert self.loop is not None | ||
| 220 | + # Guard against Zeroconf.close() being called from the eventloop | ||
| 221 | + if get_running_loop() == self.loop: | ||
| 222 | + self._async_shutdown() | ||
| 223 | + return | ||
| 224 | + if not self.loop.is_running(): | ||
| 225 | + return | ||
| 226 | + run_coro_with_timeout(self._async_close(), self.loop, _CLOSE_TIMEOUT) | ||
| 227 | + | ||
| 228 | + def setup( | ||
| 229 | + self, | ||
| 230 | + loop: asyncio.AbstractEventLoop, | ||
| 231 | + loop_thread_ready: threading.Event | None, | ||
| 232 | + ) -> None: | ||
| 233 | + """Set up the instance.""" | ||
| 234 | + self.loop = loop | ||
| 235 | + self.running_future = loop.create_future() | ||
| 236 | + self._setup_task = self.loop.create_task(self._async_setup(loop_thread_ready)) | ||
| 237 | + | ||
| 274 | 238 | async def _async_add_interface( | |
| 275 | 239 | self, | |
| 276 | 240 | interface: str | tuple[tuple[str, int, int], int], | |
@@ -309,19 +273,32 @@ async def _async_add_interface( | |||
| 309 | 273 | return False | |
| 310 | 274 | return True | |
| 311 | 275 | ||
| 312 | - def _async_remove_transport(self, transport: asyncio.DatagramTransport) -> None: | ||
| 313 | - """Drop a transport's protocol/reader/sender wrappers, cancelling its timers.""" | ||
| 314 | - kept_protocols = [] | ||
| 315 | - for protocol in self.protocols: | ||
| 316 | - if protocol.transport is not None and protocol.transport.transport is transport: | ||
| 317 | - # Cancel any pending TC-reassembly timers so one can't fire a | ||
| 318 | - # response against the transport we're about to close. | ||
| 319 | - protocol.cancel_pending_timers() | ||
| 320 | - else: | ||
| 321 | - kept_protocols.append(protocol) | ||
| 322 | - self.protocols = kept_protocols | ||
| 323 | - self.readers = _without_transport(self.readers, transport) | ||
| 324 | - self.senders = _without_transport(self.senders, transport) | ||
| 276 | + def _async_cache_cleanup(self) -> None: | ||
| 277 | + """Periodic cache cleanup.""" | ||
| 278 | + now = current_time_millis() | ||
| 279 | + self.zc.question_history.async_expire(now) | ||
| 280 | + self.zc.record_manager.async_updates( | ||
| 281 | + now, | ||
| 282 | + [RecordUpdate(record, record) for record in self.zc.cache.async_expire(now)], | ||
| 283 | + ) | ||
| 284 | + self.zc.record_manager.async_updates_complete(False) | ||
| 285 | + self._async_schedule_next_cache_cleanup() | ||
| 286 | + | ||
| 287 | + async def _async_close(self) -> None: | ||
| 288 | + """Cancel and wait for the cleanup task to finish.""" | ||
| 289 | + assert self._setup_task is not None | ||
| 290 | + # Swallow CancelledError only if the setup task itself was | ||
| 291 | + # cancelled (close-before-start); outer-task cancellation must | ||
| 292 | + # propagate. | ||
| 293 | + try: | ||
| 294 | + await self._setup_task | ||
| 295 | + except asyncio.CancelledError: | ||
| 296 | + if not self._setup_task.cancelled(): | ||
| 297 | + raise | ||
| 298 | + self._async_shutdown() | ||
| 299 | + await asyncio.sleep(0) # flush out any call soons | ||
| 300 | + if self._cleanup_timer is not None: | ||
| 301 | + self._cleanup_timer.cancel() | ||
| 325 | 302 | ||
| 326 | 303 | def _async_close_sender(self, wrapped: _WrappedTransport, listen_socket: socket.socket | None) -> None: | |
| 327 | 304 | """Drop a per-interface sender's wrappers/protocol and close its transport.""" | |
@@ -334,6 +311,31 @@ def _async_close_sender(self, wrapped: _WrappedTransport, listen_socket: socket. | |||
| 334 | 311 | # Release the socket even if a non-benign leave (e.g. EPERM) raises. | |
| 335 | 312 | transport.close() | |
| 336 | 313 | ||
| 314 | + async def _async_create_endpoints(self) -> None: | ||
| 315 | + """Create endpoints to send and receive.""" | ||
| 316 | + reader_sockets = [] | ||
| 317 | + sender_sockets = [] | ||
| 318 | + if self._listen_socket: | ||
| 319 | + reader_sockets.append(self._listen_socket) | ||
| 320 | + for s in self._respond_sockets: | ||
| 321 | + if s not in reader_sockets: | ||
| 322 | + reader_sockets.append(s) | ||
| 323 | + sender_sockets.append(s) | ||
| 324 | + | ||
| 325 | + for s in reader_sockets: | ||
| 326 | + reader = await self._async_wrap_socket(s, s in sender_sockets) | ||
| 327 | + # _async_wrap_socket registers the transport with no await between | ||
| 328 | + # creating and registering it, and the pending-handle cleanup below | ||
| 329 | + # adds no await either, so a concurrent shutdown always sees ``s`` | ||
| 330 | + # in exactly one place. | ||
| 331 | + if s is self._listen_socket: | ||
| 332 | + # Keep a handle to the shared listen socket so interface | ||
| 333 | + # rescans can add/drop multicast memberships on it. | ||
| 334 | + self._listen_transport = reader | ||
| 335 | + self._listen_socket = None | ||
| 336 | + if s in self._respond_sockets: | ||
| 337 | + self._respond_sockets.remove(s) | ||
| 338 | + | ||
| 337 | 339 | async def _async_rebuild_listen_socket( | |
| 338 | 340 | self, | |
| 339 | 341 | apple_p2p: bool, | |
@@ -387,38 +389,35 @@ async def _async_rebuild_listen_socket( | |||
| 387 | 389 | self._async_remove_transport(old_transport) | |
| 388 | 390 | old_transport.close() | |
| 389 | 391 | ||
| 390 | - def _async_cache_cleanup(self) -> None: | ||
| 391 | - """Periodic cache cleanup.""" | ||
| 392 | - now = current_time_millis() | ||
| 393 | - self.zc.question_history.async_expire(now) | ||
| 394 | - self.zc.record_manager.async_updates( | ||
| 395 | - now, | ||
| 396 | - [RecordUpdate(record, record) for record in self.zc.cache.async_expire(now)], | ||
| 397 | - ) | ||
| 398 | - self.zc.record_manager.async_updates_complete(False) | ||
| 399 | - self._async_schedule_next_cache_cleanup() | ||
| 392 | + def _async_remove_transport(self, transport: asyncio.DatagramTransport) -> None: | ||
| 393 | + """Drop a transport's protocol/reader/sender wrappers, cancelling its timers.""" | ||
| 394 | + kept_protocols = [] | ||
| 395 | + for protocol in self.protocols: | ||
| 396 | + if protocol.transport is not None and protocol.transport.transport is transport: | ||
| 397 | + # Cancel any pending TC-reassembly timers so one can't fire a | ||
| 398 | + # response against the transport we're about to close. | ||
| 399 | + protocol.cancel_pending_timers() | ||
| 400 | + else: | ||
| 401 | + kept_protocols.append(protocol) | ||
| 402 | + self.protocols = kept_protocols | ||
| 403 | + self.readers = _without_transport(self.readers, transport) | ||
| 404 | + self.senders = _without_transport(self.senders, transport) | ||
| 400 | 405 | ||
| 401 | 406 | def _async_schedule_next_cache_cleanup(self) -> None: | |
| 402 | 407 | """Schedule the next cache cleanup.""" | |
| 403 | 408 | loop = self.loop | |
| 404 | 409 | assert loop is not None | |
| 405 | 410 | self._cleanup_timer = loop.call_at(loop.time() + _CACHE_CLEANUP_INTERVAL, self._async_cache_cleanup) | |
| 406 | 411 | ||
| 407 | - async def _async_close(self) -> None: | ||
| 408 | - """Cancel and wait for the cleanup task to finish.""" | ||
| 409 | - assert self._setup_task is not None | ||
| 410 | - # Swallow CancelledError only if the setup task itself was | ||
| 411 | - # cancelled (close-before-start); outer-task cancellation must | ||
| 412 | - # propagate. | ||
| 413 | - try: | ||
| 414 | - await self._setup_task | ||
| 415 | - except asyncio.CancelledError: | ||
| 416 | - if not self._setup_task.cancelled(): | ||
| 417 | - raise | ||
| 418 | - self._async_shutdown() | ||
| 419 | - await asyncio.sleep(0) # flush out any call soons | ||
| 420 | - if self._cleanup_timer is not None: | ||
| 421 | - self._cleanup_timer.cancel() | ||
| 412 | + async def _async_setup(self, loop_thread_ready: threading.Event | None) -> None: | ||
| 413 | + """Set up the instance.""" | ||
| 414 | + self._async_schedule_next_cache_cleanup() | ||
| 415 | + await self._async_create_endpoints() | ||
| 416 | + assert self.running_future is not None | ||
| 417 | + if not self.running_future.done(): | ||
| 418 | + self.running_future.set_result(True) | ||
| 419 | + if loop_thread_ready: | ||
| 420 | + loop_thread_ready.set() | ||
| 422 | 421 | ||
| 423 | 422 | def _async_shutdown(self) -> None: | |
| 424 | 423 | """Shutdown transports and sockets; safe to call repeatedly.""" | |
@@ -439,18 +438,19 @@ def _async_shutdown(self) -> None: | |||
| 439 | 438 | s.close() | |
| 440 | 439 | self._respond_sockets = [] | |
| 441 | 440 | ||
| 442 | - def close(self) -> None: | ||
| 443 | - """Close from sync context. | ||
| 444 | - | ||
| 445 | - While it is not expected during normal operation, | ||
| 446 | - this function may raise EventLoopBlocked if the underlying | ||
| 447 | - call to `_async_close` cannot be completed. | ||
| 448 | - """ | ||
| 441 | + async def _async_wrap_socket(self, sock: socket.socket, is_sender: bool) -> _WrappedTransport: | ||
| 442 | + """Adopt a socket into a transport, register it, and return the reader wrapper.""" | ||
| 449 | 443 | assert self.loop is not None | |
| 450 | - # Guard against Zeroconf.close() being called from the eventloop | ||
| 451 | - if get_running_loop() == self.loop: | ||
| 452 | - self._async_shutdown() | ||
| 453 | - return | ||
| 454 | - if not self.loop.is_running(): | ||
| 455 | - return | ||
| 456 | - run_coro_with_timeout(self._async_close(), self.loop, _CLOSE_TIMEOUT) | ||
| 444 | + transport, protocol = await self.loop.create_datagram_endpoint( # type: ignore[type-var] | ||
| 445 | + lambda: AsyncListener(self.zc), # type: ignore[arg-type, return-value] | ||
| 446 | + sock=sock, | ||
| 447 | + ) | ||
| 448 | + datagram_transport = cast(asyncio.DatagramTransport, transport) | ||
| 449 | + reader = make_wrapped_transport(datagram_transport) | ||
| 450 | + # No ``await`` between wrapping and registering so a concurrent | ||
| 451 | + # shutdown always sees the transport in exactly one place. | ||
| 452 | + self.protocols.append(cast(AsyncListener, protocol)) | ||
| 453 | + self.readers.append(reader) | ||
| 454 | + if is_sender: | ||
| 455 | + self.senders.append(make_wrapped_transport(datagram_transport)) | ||
| 456 | + return reader | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -73,12 +73,6 @@ def async_add(self, now: _float, answers: _AnswerWithAdditionalsType) -> None: | |||
| 73 | 73 | loop.call_at(loop.time() + millis_to_seconds(random_delay), self.async_ready) | |
| 74 | 74 | self.queue.append(AnswerGroup(send_after, send_before, answers)) | |
| 75 | 75 | ||
| 76 | - def _remove_answers_from_queue(self, answers: _AnswerWithAdditionalsType) -> None: | ||
| 77 | - """Remove a set of answers from the outgoing queue.""" | ||
| 78 | - for pending in self.queue: | ||
| 79 | - for record in answers: | ||
| 80 | - pending.answers.pop(record, None) | ||
| 81 | - | ||
| 82 | 76 | def async_ready(self) -> None: | |
| 83 | 77 | """Process anything in the queue that is ready.""" | |
| 84 | 78 | zc = self.zc | |
@@ -113,3 +107,9 @@ def async_ready(self) -> None: | |||
| 113 | 107 | # If we have the same answer scheduled to go out, remove them | |
| 114 | 108 | self._remove_answers_from_queue(answers) | |
| 115 | 109 | zc.async_send(construct_outgoing_multicast_answers(answers)) | |
| 110 | + | ||
| 111 | + def _remove_answers_from_queue(self, answers: _AnswerWithAdditionalsType) -> None: | ||
| 112 | + """Remove a set of answers from the outgoing queue.""" | ||
| 113 | + for pending in self.queue: | ||
| 114 | + for record in answers: | ||
| 115 | + pending.answers.pop(record, None) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments