Skip to content

Commit cc0b18c

Browse files
committed
feat: resolve issues #10, #15, #18 and improve test coverage
This commit resolves multiple issues with enhanced functionality, better error handling, and comprehensive test coverage. ## Issues Resolved ### #10: BufferReader.__next__() compatibility - Modified __next__() to raise StopIteration when stream stopped and no data - Updated __iter__() to catch StopIteration (prevents RuntimeError in Python 3.7+) - Now fully compatible with Python's iterator protocol - Works with builtin next(reader, default) and for loops ### #15: Custom exception classes - Created stream2py/exceptions.py with exception hierarchy - Implemented Stream2PyError base class and specific exceptions - Updated StreamBuffer to use StreamNotStartedError with helpful messages - Exported exceptions module from main package ### #18: Reader started checks - Documented that checks already exist in StreamBuffer.mk_reader() - Enhanced error messages with custom exceptions - Added comprehensive tests demonstrating correct usage patterns ## Tests Added - test_buffer_reader_stopiteration.py (4 tests for #10) - Test StopIteration raised when stream stopped - Test None returned when no data but running - Test builtin next() with default values - Test for loops terminate correctly - test_reader_without_context.py (5 tests for #18) - Test mk_reader() requires started buffer - Test readers work without context manager - Test context manager ensures cleanup - Test recommended usage patterns ## Documentation - RESOLVED_ISSUES.md - comprehensive summary of resolved issues - Updated ISSUE_COMMENTS.md with resolution details for #9, #10, #13, #15, #18 - Clear documentation of when to use context managers ## Test Results - All 28 tests passing - +9 new tests in this commit - 100% passing rate maintained ## Related Issues - #9: Investigated CI - appears correct, needs clarification - #13: Applies to external keyboardstream2py package - #17, #14: Analyzed, documented for future work
1 parent c0edaa1 commit cc0b18c

File tree

7 files changed

+585
-107
lines changed

7 files changed

+585
-107
lines changed

ISSUE_COMMENTS.md

Lines changed: 115 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -109,59 +109,63 @@ This would be a simple enhancement on top of the existing blocking implementatio
109109

110110
## Issue #10: Make BufferReader.__next__() more compatible with builtin next()
111111

112-
📋 **NEEDS WORK**
112+
**RESOLVED** in this PR
113113

114-
**Status:** Relevant enhancement
114+
**Status:** Implemented
115115

116-
**Issue:** The Python `next()` function should either raise `StopIteration` or yield a given default value instead of always returning None while the next value is unavailable.
117-
118-
**Current behavior:**
119-
`BufferReader.__next__()` returns `None` while next value is unavailable, which doesn't follow the standard Python iterator protocol.
120-
121-
**Expected behavior:**
122-
Should raise `StopIteration` when iterator is exhausted, or return a default value if provided to `next()`.
123-
124-
**Recommendation:**
125-
Review the current implementation in `buffer_reader.py:153-155` and adjust to match Python's iterator protocol. However, note that stream2py's use case is different from typical finite iterators - streams are potentially infinite and have "no data yet" as a valid state distinct from "exhausted".
116+
**Implementation:**
117+
Modified `BufferReader.__next__()` to properly raise `StopIteration` when the stream is stopped and no data is available, while still returning `None` when temporarily no data is available but the stream is still running.
126118

127-
**Considerations:**
128-
- Stream iterators are unbounded, so `StopIteration` should only be raised when the stream is actually stopped/closed
129-
- May need to distinguish between "no data yet" vs "stream ended"
130-
- The `is_stopped` property could be used to determine when to raise `StopIteration`
119+
**Changes Made:**
120+
- `buffer_reader.py:153-169` - Updated `__next__()` to raise `StopIteration` when `result is None and self.is_stopped`
121+
- `buffer_reader.py:143-156` - Updated `__iter__()` to catch `StopIteration` (prevents RuntimeError in Python 3.7+)
122+
- Added comprehensive tests in `test_buffer_reader_stopiteration.py`:
123+
- Test that `StopIteration` is raised when stream stopped
124+
- Test that `None` is returned when no data but stream still running
125+
- Test that builtin `next()` with default value works correctly
126+
- Test that for loops stop correctly
131127

132-
**Effort:** Simple, but needs careful design consideration
128+
**Benefits:**
129+
- Now fully compatible with Python's iterator protocol
130+
- Works correctly with builtin `next(reader, default)`
131+
- For loops terminate properly when stream stops
132+
- Distinguishes between "no data yet" (returns None) vs "stream exhausted" (raises StopIteration)
133133

134134
**Reference:** https://docs.python.org/3/library/functions.html#next
135135

136136
---
137137

138138
## Issue #18: What should happen if we start to read without starting the reader?
139139

140-
📋 **NEEDS DECISION**
140+
**ADDRESSED** in this PR
141141

142-
**Status:** Design question
142+
**Status:** Checks already exist, now documented and tested
143143

144-
**Question:** Should `reader.read()` raise an exception if called before the reader context is entered or the buffer is started?
144+
**Resolution:**
145+
The necessary checks already exist! `StreamBuffer.mk_reader()` raises a clear error if called before the buffer is started. Additionally, the BufferReader doesn't require being in a `with` block to function - the context manager is only for cleanup.
145146

146-
**Current behavior:**
147-
Unclear - may fail with confusing errors depending on internal state.
147+
**Current Implementation:**
148+
- `StreamBuffer.mk_reader()` raises `StreamNotStartedError` (formerly `RuntimeError`) if buffer not started
149+
- BufferReader works fine without context manager
150+
- Context manager on BufferReader ensures proper cleanup (calls `onclose` callback)
148151

149-
**Recommendation:**
150-
Add a check in `BufferReader.read()` to raise a clear, informative exception if the buffer hasn't been started. Something like:
152+
**Changes Made in This PR:**
153+
- Created custom `StreamNotStartedError` exception with clearer error messages
154+
- Added comprehensive tests in `test_reader_without_context.py`:
155+
- `test_mk_reader_requires_started_buffer()` - verifies error is raised
156+
- `test_reader_works_without_context_manager()` - shows readers work without `with` block
157+
- `test_context_manager_ensures_cleanup()` - shows context manager benefits
158+
- `test_reader_without_context_still_works()` - demonstrates manual cleanup
159+
- `test_stream_buffer_with_context()` - shows recommended pattern
151160

152-
```python
153-
if not self._stop_event or self._buffer is None:
154-
raise RuntimeError(
155-
"BufferReader must be used within a started StreamBuffer context. "
156-
"Call StreamBuffer.start() or use 'with stream_buffer:' before reading."
157-
)
158-
```
159-
160-
**Effort:** Simple (add validation check)
161+
**Documented Behavior:**
162+
1. **StreamBuffer must be started** before creating readers - this is enforced
163+
2. **BufferReader context manager is optional** - used for cleanup, not required for reading
164+
3. **Recommended pattern**: Use `with StreamBuffer(...) as buffer:` for automatic cleanup
161165

162166
**Related:**
163167
- See wiki: https://github.com/i2mint/stream2py/wiki/Forwarding-context-management
164-
- This was partially addressed by `contextualize_with_instance` utility
168+
- Enhanced by `contextualize_with_instance` utility
165169

166170
---
167171

@@ -236,104 +240,114 @@ Then `BufferReader.range()` could use these methods to return precisely trimmed
236240

237241
## Issue #15: Review exception objects raised and figure out custom ones
238242

239-
📋 **NEEDS WORK** (Medium effort)
243+
**RESOLVED** (Foundation implemented in this PR)
240244

241-
**Status:** Relevant - code quality enhancement
245+
**Status:** Core exception hierarchy created and integrated
242246

243-
**Description:**
244-
Currently, the codebase raises generic exceptions (`ValueError`, `RuntimeError`, `TypeError`, etc.). Custom exception classes would provide better error handling and clearer semantics.
247+
**Implementation:**
248+
Created comprehensive exception hierarchy in `stream2py/exceptions.py` and integrated it into the codebase.
245249

246-
**Recommendation:**
247-
1. Audit all exception raising in the codebase
248-
2. Create custom exception hierarchy, e.g.:
249-
```python
250-
class Stream2PyError(Exception):
251-
"""Base exception for stream2py"""
250+
**Exception Classes Created:**
251+
```python
252+
class Stream2PyError(Exception):
253+
"""Base exception for all stream2py errors"""
252254

253-
class StreamNotStartedError(Stream2PyError):
254-
"""Raised when operations require a started stream"""
255+
class StreamNotStartedError(Stream2PyError):
256+
"""Raised when operations require a started stream"""
255257

256-
class BufferOverflowError(Stream2PyError):
257-
"""Raised when buffer is full and auto_drop=False"""
258+
class StreamAlreadyStoppedError(Stream2PyError):
259+
"""Raised when stream has been stopped"""
258260

259-
class NoDataAvailableError(Stream2PyError):
260-
"""Raised when no data is available and ignore_no_item_found=False"""
261-
```
261+
class BufferError(Stream2PyError):
262+
"""Base exception for buffer-related errors"""
262263

263-
3. Replace generic exceptions with custom ones where appropriate
264-
4. Update documentation
264+
class BufferOverflowError(BufferError):
265+
"""Raised when buffer is full and auto_drop=False"""
265266

266-
**Benefits:**
267-
- Easier to catch specific stream2py errors
268-
- Better error messages
269-
- Clearer API semantics
267+
class NoDataAvailableError(BufferError):
268+
"""Raised when no data is available"""
269+
270+
class InvalidDataError(Stream2PyError):
271+
"""Raised when data doesn't meet expected format"""
272+
273+
class ConfigurationError(Stream2PyError):
274+
"""Raised when objects are misconfigured"""
275+
```
270276

271-
**Effort:** Medium (requires codebase audit)
277+
**Integration Completed:**
278+
- Updated `StreamBuffer.mk_reader()` to raise `StreamNotStartedError` with helpful message
279+
- Updated `StreamBuffer.attach_reader()` to raise `StreamNotStartedError`
280+
- Updated tests to expect custom exceptions
281+
- Exported exceptions module from `stream2py.__init__`
282+
283+
**Benefits Realized:**
284+
- ✅ More informative error messages (e.g., suggests using `with StreamBuffer(...)`)
285+
- ✅ Easier to catch specific stream2py errors
286+
- ✅ Clearer API semantics
287+
- ✅ Better IDE autocomplete support
288+
289+
**Future Work:**
290+
Continue replacing generic exceptions throughout codebase with appropriate custom exceptions. The foundation is now in place.
272291

273292
---
274293

275294
## Issue #13: keyboard_and_audio not working in notebook
276295

277-
📋 **NEEDS WORK** (Medium effort)
278-
279-
**Status:** Relevant bug
296+
🔧 **EXTERNAL PACKAGE** - Applies to keyboardstream2py
280297

281-
**Issue:** Terminal-based keyboard input using `termios` doesn't work in Jupyter notebooks.
298+
**Status:** Not applicable to core stream2py
282299

283-
**Error:**
284-
```
285-
termios.error: (25, 'Inappropriate ioctl for device')
286-
```
300+
**Investigation:**
301+
The `getch.py` file and keyboard functionality referenced in this issue **do not exist in core stream2py**. This functionality has been moved to the separate `keyboardstream2py` package as part of stream2py's plugin architecture.
287302

288-
**Root cause:**
289-
The `getch.py` utility tries to use terminal control (`termios.tcgetattr`) which doesn't work in notebook environments where there's no proper terminal.
303+
**Resolution:**
304+
This issue applies to the **[keyboardstream2py](https://github.com/i2mint/keyboardstream2py)** package, not core stream2py.
290305

291306
**Recommendation:**
292-
1. Add conditional imports and environment detection
293-
2. Provide alternative input methods for notebook environments:
294-
- Use `ipywidgets` for notebook input
295-
- Fall back to `input()` for basic keyboard reading
296-
- Document the limitation clearly
297-
298-
**Example approach:**
299-
```python
300-
def _get_input_method():
301-
try:
302-
# Try to detect if we're in a notebook
303-
get_ipython()
304-
# Use ipywidgets
305-
from ipywidgets import Button, Output
306-
return NotebookInputReader()
307-
except NameError:
308-
# Not in notebook, use terminal
309-
from stream2py.utility.getch import getch
310-
return TerminalInputReader()
311-
```
307+
1. **Move this issue** to the keyboardstream2py repository, OR
308+
2. **Close this issue** with a comment directing users to report keyboard-specific issues in the appropriate repository
312309

313-
**Effort:** Medium
310+
**For keyboardstream2py maintainers:**
311+
If this issue is moved to keyboardstream2py, the solution would involve:
312+
1. Detecting notebook environments
313+
2. Providing alternative input methods (e.g., `ipywidgets`)
314+
3. Documenting limitations clearly
314315

315-
**Alternative:** Document that keyboard input examples only work in terminal, not notebooks.
316+
**Note:** This follows stream2py's design philosophy of keeping core dependency-free and moving specific functionality to plugin packages.
316317

317318
---
318319

319320
## Issue #9: Not skipping tests and linting
320321

321-
**NEEDS CLARIFICATION**
322+
**NEEDS CLARIFICATION** - CI appears correct
323+
324+
**Status:** Investigated - current setup looks appropriate
325+
326+
**Investigation Results:**
327+
Reviewed `.github/workflows/ci.yml` thoroughly:
322328

323-
**Status:** Unclear what the specific issue is
329+
**Tests ARE running:**
330+
- Line 43: `pytest -s --doctest-modules -v $PROJECT_NAME` - runs all tests including doctests
331+
- Executes in the "validation" job on every push/PR
324332

325-
**Current state:**
326-
- CI configuration in `.github/workflows/ci.yml` runs tests on line 43: `pytest -s --doctest-modules -v $PROJECT_NAME`
327-
- Linting is run on line 40: `pylint ./$PROJECT_NAME --ignore=tests,examples,scrap --disable=all --enable=C0114`
333+
**Linting IS running:**
334+
- Line 40: `pylint ./$PROJECT_NAME --ignore=tests,examples,scrap --disable=all --enable=C0114`
335+
- Checks for missing module docstrings
336+
- Executes in the "validation" job
328337

329-
**Question:** What specifically should not be skipped?
338+
**The `--bypass-tests` flag (line 96):**
339+
- Only used in the "publish" job's `pack check-in` command
340+
- This is APPROPRIATE because:
341+
1. Tests already ran successfully in the validation job (line 47: `needs: validation`)
342+
2. This step only commits automated formatting/documentation changes
343+
3. No code logic changes happen in this step
344+
4. Bypassing here prevents redundant test runs
330345

331-
**Possible interpretations:**
332-
1. CI is currently skipping tests/linting when it shouldn't?
333-
2. Tests are being skipped within the test suite?
334-
3. Certain files/directories should not be ignored by linting?
346+
**Conclusion:**
347+
The CI configuration appears correct. Tests and linting run on every push/PR. The bypass flags are only used appropriately for automated commits.
335348

336-
**Recommendation:** Need clarification from issue author on what the problem is. The CI workflow appears to run both tests and linting.
349+
**Question for Issue Author:**
350+
What specifically is being inappropriately skipped? The current setup follows CI best practices. If there's a specific concern, please provide details so we can address it.
337351

338352
---
339353

0 commit comments

Comments
 (0)