FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

chore: enforce the declaration ordering convention in ci · python-zeroconf/python-zeroconf@348072c · GitHub

Repository navigation

Commit 348072c

Browse files
committed
chore: enforce the declaration ordering convention in ci
1 parent d44c3f8 commit 348072c

17 files changed

Lines changed: 736 additions & 675 deletions

‎.pre-commit-config.yaml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,11 @@ repos:
5858
hooks:
5959
- id: cython-lint
6060
- 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$

‎pyproject.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ select = [
183183
"examples/**/*" = [
184184
"T201", # intended
185185
]
186+
"scripts/**/*" = [
187+
"T201", # intended
188+
]
186189
"setup.py" = ["D100"]
187190
"conftest.py" = ["D100"]
188191
"docs/conf.py" = ["D100"]

‎scripts/check_declaration_order.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff 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())

‎src/zeroconf/_engine.py‎

Lines changed: 114 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -117,68 +117,6 @@ def __init__(
117117
self._cleanup_timer: asyncio.TimerHandle | None = None
118118
self._setup_task: asyncio.Task[None] | None = None
119119

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-
182120
async def async_update_interfaces(
183121
self,
184122
interfaces: InterfacesType,
@@ -271,6 +209,32 @@ async def async_update_interfaces(
271209
added = True
272210
return added
273211

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+
274238
async def _async_add_interface(
275239
self,
276240
interface: str | tuple[tuple[str, int, int], int],
@@ -309,19 +273,32 @@ async def _async_add_interface(
309273
return False
310274
return True
311275

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()
325302

326303
def _async_close_sender(self, wrapped: _WrappedTransport, listen_socket: socket.socket | None) -> None:
327304
"""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.
334311
# Release the socket even if a non-benign leave (e.g. EPERM) raises.
335312
transport.close()
336313

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+
337339
async def _async_rebuild_listen_socket(
338340
self,
339341
apple_p2p: bool,
@@ -387,38 +389,35 @@ async def _async_rebuild_listen_socket(
387389
self._async_remove_transport(old_transport)
388390
old_transport.close()
389391

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)
400405

401406
def _async_schedule_next_cache_cleanup(self) -> None:
402407
"""Schedule the next cache cleanup."""
403408
loop = self.loop
404409
assert loop is not None
405410
self._cleanup_timer = loop.call_at(loop.time() + _CACHE_CLEANUP_INTERVAL, self._async_cache_cleanup)
406411

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()
422421

423422
def _async_shutdown(self) -> None:
424423
"""Shutdown transports and sockets; safe to call repeatedly."""
@@ -439,18 +438,19 @@ def _async_shutdown(self) -> None:
439438
s.close()
440439
self._respond_sockets = []
441440

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."""
449443
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

‎src/zeroconf/_handlers/multicast_outgoing_queue.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,6 @@ def async_add(self, now: _float, answers: _AnswerWithAdditionalsType) -> None:
7373
loop.call_at(loop.time() + millis_to_seconds(random_delay), self.async_ready)
7474
self.queue.append(AnswerGroup(send_after, send_before, answers))
7575

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-
8276
def async_ready(self) -> None:
8377
"""Process anything in the queue that is ready."""
8478
zc = self.zc
@@ -113,3 +107,9 @@ def async_ready(self) -> None:
113107
# If we have the same answer scheduled to go out, remove them
114108
self._remove_answers_from_queue(answers)
115109
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)

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL