diff --git a/src/frequenz/channels/_select.py b/src/frequenz/channels/_select.py index 9b182f1b..a7ec4c90 100644 --- a/src/frequenz/channels/_select.py +++ b/src/frequenz/channels/_select.py @@ -17,6 +17,16 @@ _T = TypeVar("_T") +class _EmptyResult: + """A sentinel value to distinguish between None and empty result. + + We need a sentinel because a result can also be `None`. + """ + + def __repr__(self) -> str: + return "" + + class Selected(Generic[_T]): """A result of a [`select()`][frequenz.channels.select] iteration. @@ -31,15 +41,6 @@ class Selected(Generic[_T]): Please see [`select()`][frequenz.channels.select] for an example. """ - class _EmptyResult: - """A sentinel value to distinguish between None and empty result. - - We need a sentinel because a result can also be `None`. - """ - - def __repr__(self) -> str: - return "" - def __init__(self, receiver: Receiver[_T]) -> None: """Create a new instance. @@ -55,7 +56,7 @@ def __init__(self, receiver: Receiver[_T]) -> None: self._recv: Receiver[_T] = receiver """The receiver that was selected.""" - self._value: _T | Selected._EmptyResult = Selected._EmptyResult() + self._value: _T | _EmptyResult = _EmptyResult() """The value that was received. If there was an exception while receiving the value, then this will be `None`. @@ -86,7 +87,7 @@ def value(self) -> _T: """ if self._exception is not None: raise self._exception - assert not isinstance(self._value, Selected._EmptyResult) + assert not isinstance(self._value, _EmptyResult) return self._value @property diff --git a/src/frequenz/channels/file_watcher.py b/src/frequenz/channels/file_watcher.py index 5ddb6e95..64bc62e1 100644 --- a/src/frequenz/channels/file_watcher.py +++ b/src/frequenz/channels/file_watcher.py @@ -3,8 +3,6 @@ """A Channel receiver for watching for new, modified or deleted files.""" -from __future__ import annotations - import asyncio import pathlib from collections import abc @@ -17,29 +15,31 @@ from ._receiver import Receiver, ReceiverStoppedError -class FileWatcher(Receiver["FileWatcher.Event"]): - """A channel receiver that watches for file events.""" +class EventType(Enum): + """Available types of changes to watch for.""" + + CREATE = Change.added + """A new file was created.""" - class EventType(Enum): - """Available types of changes to watch for.""" + MODIFY = Change.modified + """An existing file was modified.""" - CREATE = Change.added - """A new file was created.""" + DELETE = Change.deleted + """An existing file was deleted.""" - MODIFY = Change.modified - """An existing file was modified.""" - DELETE = Change.deleted - """An existing file was deleted.""" +@dataclass(frozen=True) +class Event: + """A file change event.""" - @dataclass(frozen=True) - class Event: - """A file change event.""" + type: EventType + """The type of change that was observed.""" + path: pathlib.Path + """The path where the change was observed.""" - type: FileWatcher.EventType - """The type of change that was observed.""" - path: pathlib.Path - """The path where the change was observed.""" + +class FileWatcher(Receiver[Event]): + """A channel receiver that watches for file events.""" def __init__( self, @@ -53,7 +53,7 @@ def __init__( event_types: Types of events to watch for. Defaults to watch for all event types. """ - self.event_types: frozenset[FileWatcher.EventType] = frozenset(event_types) + self.event_types: frozenset[EventType] = frozenset(event_types) """The types of events to watch for.""" self._stop_event: asyncio.Event = asyncio.Event() @@ -133,9 +133,7 @@ def consume(self) -> Event: assert self._changes, "`consume()` must be preceded by a call to `ready()`" # Tuple of (Change, path) returned by watchfiles change, path_str = self._changes.pop() - return FileWatcher.Event( - type=FileWatcher.EventType(change), path=pathlib.Path(path_str) - ) + return Event(type=EventType(change), path=pathlib.Path(path_str)) def __str__(self) -> str: """Return a string representation of this receiver.""" diff --git a/tests/test_file_watcher.py b/tests/test_file_watcher.py index 2071b9da..c1a65838 100644 --- a/tests/test_file_watcher.py +++ b/tests/test_file_watcher.py @@ -15,7 +15,7 @@ from watchfiles import Change from watchfiles.main import FileChange -from frequenz.channels.file_watcher import FileWatcher +from frequenz.channels.file_watcher import Event, EventType, FileWatcher class _FakeAwatch: @@ -74,14 +74,14 @@ async def test_file_watcher_receive_updates( for change in changes: recv_changes = await file_watcher.receive() - event_type = FileWatcher.EventType(change[0]) + event_type = EventType(change[0]) path = pathlib.Path(change[1]) - assert recv_changes == FileWatcher.Event(type=event_type, path=path) + assert recv_changes == Event(type=event_type, path=path) -@hypothesis.given(event_types=st.sets(st.sampled_from(FileWatcher.EventType))) +@hypothesis.given(event_types=st.sets(st.sampled_from(EventType))) async def test_file_watcher_filter_events( - event_types: set[FileWatcher.EventType], + event_types: set[EventType], ) -> None: """Test the file watcher events filtering.""" good_path = "good-file" @@ -100,7 +100,7 @@ async def test_file_watcher_filter_events( pathlib.Path(good_path), stop_event=mock.ANY, watch_filter=filter_events ) ] - for event_type in FileWatcher.EventType: + for event_type in EventType: assert filter_events(event_type.value, good_path) == ( event_type in event_types ) diff --git a/tests/test_file_watcher_integration.py b/tests/test_file_watcher_integration.py index 5c9ab935..754aca5f 100644 --- a/tests/test_file_watcher_integration.py +++ b/tests/test_file_watcher_integration.py @@ -10,7 +10,7 @@ import pytest from frequenz.channels import select, selected_from -from frequenz.channels.file_watcher import FileWatcher +from frequenz.channels.file_watcher import Event, EventType, FileWatcher from frequenz.channels.timer import Timer @@ -33,12 +33,8 @@ async def test_file_watcher(tmp_path: pathlib.Path) -> None: if selected_from(selected, timer): filename.write_text(f"{selected.value}") elif selected_from(selected, file_watcher): - event_type = ( - FileWatcher.EventType.CREATE - if number_of_writes == 0 - else FileWatcher.EventType.MODIFY - ) - assert selected.value == FileWatcher.Event(type=event_type, path=filename) + event_type = EventType.CREATE if number_of_writes == 0 else EventType.MODIFY + assert selected.value == Event(type=event_type, path=filename) number_of_writes += 1 # After receiving a write 3 times, unsubscribe from the writes channel if number_of_writes == expected_number_of_writes: @@ -58,9 +54,7 @@ async def test_file_watcher_deletes(tmp_path: pathlib.Path) -> None: tmp_path: A tmp directory to run the file watcher on. Created by pytest. """ filename = tmp_path / "test-file" - file_watcher = FileWatcher( - paths=[str(tmp_path)], event_types={FileWatcher.EventType.DELETE} - ) + file_watcher = FileWatcher(paths=[str(tmp_path)], event_types={EventType.DELETE}) write_timer = Timer.timeout(timedelta(seconds=0.1)) deletion_timer = Timer.timeout(timedelta(seconds=0.25))