Skip to content

Commit 385d2f1

Browse files
committed
chore: add coverage
1 parent 4df7840 commit 385d2f1

File tree

7 files changed

+90
-18
lines changed

7 files changed

+90
-18
lines changed

.github/workflows/ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ jobs:
4040
- "3.10"
4141
- "3.11"
4242
- "3.12"
43+
- "3.13"
4344
os:
4445
- ubuntu-latest
4546
- windows-latest

pyproject.toml

+6
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,9 @@ match = "main"
160160
[tool.semantic_release.branches.noop]
161161
match = "(?!main$)"
162162
prerelease = true
163+
164+
[tool.uv]
165+
dev-dependencies = [
166+
"pytest-asyncio>=0.25.2",
167+
"pytest-cov>=6.0.0",
168+
]

src/aiousbwatcher/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__version__ = "0.0.0"
22

3-
from .impl import AIOUSBWatcher
3+
from .impl import AIOUSBWatcher, InotifyNotAvailableError
44

5-
__all__ = ["AIOUSBWatcher"]
5+
__all__ = ["AIOUSBWatcher", "InotifyNotAvailableError"]

src/aiousbwatcher/impl.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@
77
from pathlib import Path
88
from typing import Callable
99

10-
from asyncinotify import Inotify, Mask
11-
12-
_BASE_MASK = (
13-
Mask.MOVED_FROM | Mask.MOVED_TO | Mask.CREATE | Mask.DELETE_SELF | Mask.IGNORED
14-
)
1510
_PATH = "/dev/bus/usb"
1611

1712
_LOGGER = logging.getLogger(__name__)
1813

1914

15+
class InotifyNotAvailableError(Exception):
16+
"""Raised when inotify is not available on the platform."""
17+
18+
2019
def _get_directories_recursive_gen(path: Path) -> Generator[Path, None, None]:
2120
if path.is_dir():
2221
yield path
@@ -38,9 +37,7 @@ class AIOUSBWatcher:
3837
"""A watcher for USB devices that uses asyncio."""
3938

4039
def __init__(self) -> None:
41-
self._inotify = Inotify()
4240
self._path = Path(_PATH)
43-
self._mask = Mask.CREATE | _BASE_MASK
4441
self._loop = asyncio.get_running_loop()
4542
self._task: asyncio.Task[None] | None = None
4643
self._callbacks: set[Callable[[], None]] = set()
@@ -49,6 +46,12 @@ def async_start(self) -> Callable[[], None]:
4946
"""Start the watcher."""
5047
if self._task is not None:
5148
raise RuntimeError("Watcher already started")
49+
try:
50+
from asyncinotify import Inotify # noqa
51+
except AttributeError as ex:
52+
raise InotifyNotAvailableError(
53+
"Inotify not available on this platform"
54+
) from ex
5255
self._task = self._loop.create_task(self._watcher())
5356
return self._async_stop
5457

@@ -66,22 +69,33 @@ def _async_stop(self) -> None:
6669
self._task = None
6770

6871
async def _watcher(self) -> None:
72+
from asyncinotify import Inotify, Mask
73+
74+
mask = (
75+
Mask.CREATE
76+
| Mask.MOVED_FROM
77+
| Mask.MOVED_TO
78+
| Mask.CREATE
79+
| Mask.DELETE_SELF
80+
| Mask.IGNORED
81+
)
82+
6983
with Inotify() as inotify:
7084
for directory in await _async_get_directories_recursive(
7185
self._loop, self._path
7286
):
73-
inotify.add_watch(directory, self._mask)
87+
inotify.add_watch(directory, mask)
7488

7589
async for event in inotify:
7690
# Add subdirectories to watch if a new directory is added.
7791
if Mask.CREATE in event.mask and event.path is not None:
7892
for directory in await _async_get_directories_recursive(
7993
self._loop, event.path
8094
):
81-
inotify.add_watch(directory, self._mask)
95+
inotify.add_watch(directory, mask)
8296

8397
# If there is at least some overlap, assume the user wants this event.
84-
if event.mask & self._mask:
98+
if event.mask & mask:
8599
self._async_call_callbacks()
86100

87101
def _async_unregister_callback(self, callback: Callable[[], None]) -> None:

tests/test_impl.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import asyncio
2+
from pathlib import Path
3+
from sys import platform
4+
from unittest.mock import patch
5+
6+
import pytest
7+
8+
from aiousbwatcher import AIOUSBWatcher, InotifyNotAvailableError
9+
10+
11+
@pytest.mark.asyncio
12+
@pytest.mark.skipif(platform == "linux", reason="Inotify is available on this platform")
13+
async def test_aiousbwatcher_not_available() -> None:
14+
with pytest.raises(InotifyNotAvailableError):
15+
watcher = AIOUSBWatcher()
16+
watcher.async_start()
17+
18+
19+
@pytest.mark.asyncio
20+
@pytest.mark.skipif(
21+
platform != "linux", reason="Inotify not available on this platform"
22+
)
23+
async def test_aiousbwatcher_callbacks(tmp_path: Path) -> None:
24+
called: bool = False
25+
26+
def callback() -> None:
27+
nonlocal called
28+
called = True
29+
30+
with patch("aiousbwatcher.impl._PATH", str(tmp_path)):
31+
watcher = AIOUSBWatcher()
32+
unregister = watcher.async_register_callback(callback)
33+
stop = watcher.async_start()
34+
await asyncio.sleep(0.1)
35+
assert not called
36+
(tmp_path / "test").touch()
37+
await asyncio.sleep(0.1)
38+
assert called
39+
unregister() # type: ignore[unreachable]
40+
stop()
41+
await asyncio.sleep(0.1)
42+
assert not called

tests/test_main.py

-6
This file was deleted.

uv.lock

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)