Skip to content

Commit dd756d7

Browse files
committed
Move nested class out of the parent class
Removing nested classes avoids having to use hacky constructions, like requiring the use of `from __future__ import annotations`, types as strings and confusing the `mkdocstrings` tools when extracting and cross-linking docs. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 25b38ba commit dd756d7

File tree

4 files changed

+43
-50
lines changed

4 files changed

+43
-50
lines changed

src/frequenz/channels/_select.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@
1717
_T = TypeVar("_T")
1818

1919

20+
class _EmptyResult:
21+
"""A sentinel value to distinguish between None and empty result.
22+
23+
We need a sentinel because a result can also be `None`.
24+
"""
25+
26+
def __repr__(self) -> str:
27+
return "<empty>"
28+
29+
2030
class Selected(Generic[_T]):
2131
"""A result of a [`select()`][frequenz.channels.select] iteration.
2232
@@ -31,15 +41,6 @@ class Selected(Generic[_T]):
3141
Please see [`select()`][frequenz.channels.select] for an example.
3242
"""
3343

34-
class _EmptyResult:
35-
"""A sentinel value to distinguish between None and empty result.
36-
37-
We need a sentinel because a result can also be `None`.
38-
"""
39-
40-
def __repr__(self) -> str:
41-
return "<empty>"
42-
4344
def __init__(self, receiver: Receiver[_T]) -> None:
4445
"""Create a new instance.
4546
@@ -55,7 +56,7 @@ def __init__(self, receiver: Receiver[_T]) -> None:
5556
self._recv: Receiver[_T] = receiver
5657
"""The receiver that was selected."""
5758

58-
self._value: _T | Selected._EmptyResult = Selected._EmptyResult()
59+
self._value: _T | _EmptyResult = _EmptyResult()
5960
"""The value that was received.
6061
6162
If there was an exception while receiving the value, then this will be `None`.
@@ -86,7 +87,7 @@ def value(self) -> _T:
8687
"""
8788
if self._exception is not None:
8889
raise self._exception
89-
assert not isinstance(self._value, Selected._EmptyResult)
90+
assert not isinstance(self._value, _EmptyResult)
9091
return self._value
9192

9293
@property

src/frequenz/channels/file_watcher.py

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
"""A Channel receiver for watching for new, modified or deleted files."""
55

6-
from __future__ import annotations
7-
86
import asyncio
97
import pathlib
108
from collections import abc
@@ -17,29 +15,31 @@
1715
from ._receiver import Receiver, ReceiverStoppedError
1816

1917

20-
class FileWatcher(Receiver["FileWatcher.Event"]):
21-
"""A channel receiver that watches for file events."""
18+
class EventType(Enum):
19+
"""Available types of changes to watch for."""
20+
21+
CREATE = Change.added
22+
"""A new file was created."""
2223

23-
class EventType(Enum):
24-
"""Available types of changes to watch for."""
24+
MODIFY = Change.modified
25+
"""An existing file was modified."""
2526

26-
CREATE = Change.added
27-
"""A new file was created."""
27+
DELETE = Change.deleted
28+
"""An existing file was deleted."""
2829

29-
MODIFY = Change.modified
30-
"""An existing file was modified."""
3130

32-
DELETE = Change.deleted
33-
"""An existing file was deleted."""
31+
@dataclass(frozen=True)
32+
class Event:
33+
"""A file change event."""
3434

35-
@dataclass(frozen=True)
36-
class Event:
37-
"""A file change event."""
35+
type: EventType
36+
"""The type of change that was observed."""
37+
path: pathlib.Path
38+
"""The path where the change was observed."""
3839

39-
type: FileWatcher.EventType
40-
"""The type of change that was observed."""
41-
path: pathlib.Path
42-
"""The path where the change was observed."""
40+
41+
class FileWatcher(Receiver[Event]):
42+
"""A channel receiver that watches for file events."""
4343

4444
def __init__(
4545
self,
@@ -53,7 +53,7 @@ def __init__(
5353
event_types: Types of events to watch for. Defaults to watch for
5454
all event types.
5555
"""
56-
self.event_types: frozenset[FileWatcher.EventType] = frozenset(event_types)
56+
self.event_types: frozenset[EventType] = frozenset(event_types)
5757
"""The types of events to watch for."""
5858

5959
self._stop_event: asyncio.Event = asyncio.Event()
@@ -133,9 +133,7 @@ def consume(self) -> Event:
133133
assert self._changes, "`consume()` must be preceded by a call to `ready()`"
134134
# Tuple of (Change, path) returned by watchfiles
135135
change, path_str = self._changes.pop()
136-
return FileWatcher.Event(
137-
type=FileWatcher.EventType(change), path=pathlib.Path(path_str)
138-
)
136+
return Event(type=EventType(change), path=pathlib.Path(path_str))
139137

140138
def __str__(self) -> str:
141139
"""Return a string representation of this receiver."""

tests/test_file_watcher.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from watchfiles import Change
1616
from watchfiles.main import FileChange
1717

18-
from frequenz.channels.file_watcher import FileWatcher
18+
from frequenz.channels.file_watcher import Event, EventType, FileWatcher
1919

2020

2121
class _FakeAwatch:
@@ -74,14 +74,14 @@ async def test_file_watcher_receive_updates(
7474

7575
for change in changes:
7676
recv_changes = await file_watcher.receive()
77-
event_type = FileWatcher.EventType(change[0])
77+
event_type = EventType(change[0])
7878
path = pathlib.Path(change[1])
79-
assert recv_changes == FileWatcher.Event(type=event_type, path=path)
79+
assert recv_changes == Event(type=event_type, path=path)
8080

8181

82-
@hypothesis.given(event_types=st.sets(st.sampled_from(FileWatcher.EventType)))
82+
@hypothesis.given(event_types=st.sets(st.sampled_from(EventType)))
8383
async def test_file_watcher_filter_events(
84-
event_types: set[FileWatcher.EventType],
84+
event_types: set[EventType],
8585
) -> None:
8686
"""Test the file watcher events filtering."""
8787
good_path = "good-file"
@@ -100,7 +100,7 @@ async def test_file_watcher_filter_events(
100100
pathlib.Path(good_path), stop_event=mock.ANY, watch_filter=filter_events
101101
)
102102
]
103-
for event_type in FileWatcher.EventType:
103+
for event_type in EventType:
104104
assert filter_events(event_type.value, good_path) == (
105105
event_type in event_types
106106
)

tests/test_file_watcher_integration.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import pytest
1111

1212
from frequenz.channels import select, selected_from
13-
from frequenz.channels.file_watcher import FileWatcher
13+
from frequenz.channels.file_watcher import Event, EventType, FileWatcher
1414
from frequenz.channels.timer import Timer
1515

1616

@@ -33,12 +33,8 @@ async def test_file_watcher(tmp_path: pathlib.Path) -> None:
3333
if selected_from(selected, timer):
3434
filename.write_text(f"{selected.value}")
3535
elif selected_from(selected, file_watcher):
36-
event_type = (
37-
FileWatcher.EventType.CREATE
38-
if number_of_writes == 0
39-
else FileWatcher.EventType.MODIFY
40-
)
41-
assert selected.value == FileWatcher.Event(type=event_type, path=filename)
36+
event_type = EventType.CREATE if number_of_writes == 0 else EventType.MODIFY
37+
assert selected.value == Event(type=event_type, path=filename)
4238
number_of_writes += 1
4339
# After receiving a write 3 times, unsubscribe from the writes channel
4440
if number_of_writes == expected_number_of_writes:
@@ -58,9 +54,7 @@ async def test_file_watcher_deletes(tmp_path: pathlib.Path) -> None:
5854
tmp_path: A tmp directory to run the file watcher on. Created by pytest.
5955
"""
6056
filename = tmp_path / "test-file"
61-
file_watcher = FileWatcher(
62-
paths=[str(tmp_path)], event_types={FileWatcher.EventType.DELETE}
63-
)
57+
file_watcher = FileWatcher(paths=[str(tmp_path)], event_types={EventType.DELETE})
6458
write_timer = Timer.timeout(timedelta(seconds=0.1))
6559
deletion_timer = Timer.timeout(timedelta(seconds=0.25))
6660

0 commit comments

Comments
 (0)