-
-
Notifications
You must be signed in to change notification settings - Fork 1
Handle missing SIGHUP on Windows #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Use EAFP pattern with try/except for signal handling - Add Windows-specific SIGBREAK signal support - Use match-case for OS-specific signal registration - Add comprehensive tests for signal handling across platforms - Update pyproject.toml with Windows/macOS classifiers - Extend CI to run on Windows and macOS Co-authored-by: codingjoe <[email protected]>
Remove conflicting 'OS Independent' classifier and use more general 'POSIX' instead of 'POSIX :: Linux' to better reflect actual OS support. Co-authored-by: codingjoe <[email protected]>
Add explicit permissions block to limit GITHUB_TOKEN permissions as a security best practice. Co-authored-by: codingjoe <[email protected]>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #22 +/- ##
==========================================
+ Coverage 98.54% 98.59% +0.05%
==========================================
Files 5 5
Lines 137 142 +5
==========================================
+ Hits 135 140 +5
Misses 2 2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
codingjoe
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot ditch the comments. The code is pretty easy to read.
- Remove all comments from signal handling code - Simplify pytest-windows-macos to test only Django 5.2 LTS and Python 3.13 Co-authored-by: codingjoe <[email protected]>
codingjoe
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request addresses the missing SIGHUP signal on Windows by implementing platform-specific signal handling in the crontask management command. The solution uses Python's match-case statement to register different signals based on the operating system, with graceful error handling for signals that may not be available.
Changes:
- Implemented platform-aware signal registration with EAFP pattern for unavailable signals
- Added comprehensive test coverage for signal handling on different platforms
- Extended CI pipeline to test on Windows and macOS in addition to Linux
- Updated pyproject.toml classifiers to reflect multi-platform support
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| crontask/management/commands/crontask.py | Added platform detection and platform-specific signal registration (SIGBREAK for Windows, SIGHUP for Unix-like systems) with graceful error handling |
| tests/test_commands.py | Added three new test methods to verify signal registration behavior on Unix, Windows, and when signals are unavailable |
| pyproject.toml | Updated OS classifiers from "OS Independent" to specific platform classifiers for Windows, macOS, and POSIX |
| .github/workflows/ci.yml | Split pytest job and added new job for Windows and macOS testing with Python 3.13 and Django 6.0 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pyproject.toml
Outdated
| "Intended Audience :: Developers", | ||
| "Operating System :: OS Independent", | ||
| "Operating System :: Microsoft :: Windows", | ||
| "Operating System :: MacOS", |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The classifier "Operating System :: MacOS" is not valid. The correct PyPI classifier is "Operating System :: MacOS :: MacOS X" according to the official PyPI classifier list.
Update this to use the correct classifier format.
| "Operating System :: MacOS", | |
| "Operating System :: MacOS :: MacOS X", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 4078eee
tests/test_commands.py
Outdated
| # Mock signal.signal to raise AttributeError for SIGHUP | ||
| original_signal = signal.signal | ||
|
|
||
| def mock_signal(signum, handler): | ||
| # Simulate SIGHUP not being available | ||
| if hasattr(signal, "SIGHUP") and signum == signal.SIGHUP: | ||
| raise AttributeError("SIGHUP not available") | ||
| return original_signal(signum, handler) | ||
|
|
||
| with patch("signal.signal", side_effect=mock_signal): | ||
| with io.StringIO() as stdout: | ||
| # Should not raise an exception | ||
| call_command("crontask", stdout=stdout) | ||
| assert "Starting scheduler…" in stdout.getvalue() |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test doesn't effectively verify the platform-specific signal handling behavior. The test mocks signal.signal to raise AttributeError when registering SIGHUP, but the actual implementation in crontask.py uses a match statement on sys.platform to determine which signals to register. The test doesn't mock sys.platform, so it will execute the default branch (non-Windows) which tries to register SIGHUP.
However, the mock on line 143-147 checks if hasattr(signal, "SIGHUP") which will be True on Unix systems (where tests typically run), causing it to raise an error. But this doesn't match the real scenario where SIGHUP simply doesn't exist as an attribute on Windows. Consider restructuring this test to more accurately simulate the Windows environment by mocking both sys.platform and removing the SIGHUP attribute from the signal module.
| # Mock signal.signal to raise AttributeError for SIGHUP | |
| original_signal = signal.signal | |
| def mock_signal(signum, handler): | |
| # Simulate SIGHUP not being available | |
| if hasattr(signal, "SIGHUP") and signum == signal.SIGHUP: | |
| raise AttributeError("SIGHUP not available") | |
| return original_signal(signum, handler) | |
| with patch("signal.signal", side_effect=mock_signal): | |
| with io.StringIO() as stdout: | |
| # Should not raise an exception | |
| call_command("crontask", stdout=stdout) | |
| assert "Starting scheduler…" in stdout.getvalue() | |
| # Simulate a platform where SIGHUP is not defined (e.g. Windows) | |
| with patch.object(sys, "platform", "win32"): | |
| had_sighup = hasattr(signal, "SIGHUP") | |
| original_sighup = getattr(signal, "SIGHUP", None) | |
| if had_sighup: | |
| delattr(signal, "SIGHUP") | |
| try: | |
| with io.StringIO() as stdout: | |
| # Should not raise an exception even if SIGHUP is missing | |
| call_command("crontask", stdout=stdout) | |
| assert "Starting scheduler…" in stdout.getvalue() | |
| finally: | |
| # Restore SIGHUP attribute for other tests | |
| if had_sighup: | |
| setattr(signal, "SIGHUP", original_sighup) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced with simpler OS-specific tests using @pytest.mark.skipif in 4078eee
| pytest-windows-macos: | ||
| name: Pytest | ||
| permissions: | ||
| contents: read | ||
| strategy: | ||
| matrix: | ||
| os: | ||
| - "windows-latest" | ||
| - "macos-latest" | ||
| python-version: | ||
| - "3.13" | ||
| django-version: | ||
| - "6.0" | ||
| runs-on: ${{ matrix.os }} | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: astral-sh/setup-uv@v7 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
| - run: uv run --with django~=${{ matrix.django-version }}.0 pytest |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Windows/macOS pytest job doesn't set the REDIS_URL environment variable that is set for the Linux job (line 41). While tests that require Redis are already using pytest.importorskip to skip when Redis is not available, this inconsistency could lead to confusion or unexpected behavior if any code checks for the REDIS_URL environment variable.
Consider either adding the REDIS_URL environment variable to this job (even though Redis service isn't configured), or documenting why it's not needed for these platforms.
| signal.signal(signal.SIGTERM, kill_softly) | ||
| signal.signal(signal.SIGINT, kill_softly) | ||
| case _: | ||
| try: | ||
| signal.signal(signal.SIGHUP, kill_softly) | ||
| except (AttributeError, OSError): | ||
| pass | ||
| signal.signal(signal.SIGTERM, kill_softly) | ||
| signal.signal(signal.SIGINT, kill_softly) |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The signal registration code is duplicated in both branches of the match statement. SIGTERM and SIGINT are registered identically in lines 70-71 and 77-78. This duplication reduces maintainability and increases the risk of inconsistencies.
Consider refactoring to eliminate duplication by registering the platform-specific signals first (SIGHUP for non-Windows, SIGBREAK for Windows), then registering SIGTERM and SIGINT once after the match statement.
| signal.signal(signal.SIGTERM, kill_softly) | |
| signal.signal(signal.SIGINT, kill_softly) | |
| case _: | |
| try: | |
| signal.signal(signal.SIGHUP, kill_softly) | |
| except (AttributeError, OSError): | |
| pass | |
| signal.signal(signal.SIGTERM, kill_softly) | |
| signal.signal(signal.SIGINT, kill_softly) | |
| case _: | |
| try: | |
| signal.signal(signal.SIGHUP, kill_softly) | |
| except (AttributeError, OSError): | |
| pass | |
| signal.signal(signal.SIGTERM, kill_softly) | |
| signal.signal(signal.SIGINT, kill_softly) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed duplication - moved SIGTERM and SIGINT outside match block in 4078eee
| case "win32": | ||
| try: | ||
| signal.signal(signal.SIGBREAK, kill_softly) | ||
| except (AttributeError, OSError): |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be good to know when this exception is expected, and why an OSError?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed OSError - only AttributeError is expected when a signal constant doesn't exist on a platform (e.g., SIGHUP on Windows). Fixed in 4078eee
| case _: | ||
| try: | ||
| signal.signal(signal.SIGHUP, kill_softly) | ||
| except (AttributeError, OSError): |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need an example, where this could fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed OSError - only AttributeError is expected when signal.SIGHUP doesn't exist as an attribute on the platform. Fixed in 4078eee
|
@copilot address the unresolved review comments. Furthermore, try to avoid patching and mocking where possible. You may also use OS-dependent tests via |
- Remove code duplication: move SIGTERM/SIGINT registration outside match block - Remove OSError from exception handling (only AttributeError is expected) - Fix PyPI classifier: "Operating System :: MacOS" -> "Operating System :: MacOS :: MacOS X" - Simplify tests: use @pytest.mark.skipif for OS-specific tests instead of mocking sys.platform - Remove ineffective test_launch_scheduler_missing_sighup test Co-authored-by: codingjoe <[email protected]>
Addressed all review comments in 4078eee:
|
|
pre-commit.ci run |
for more information, see https://pre-commit.ci
Plan to Handle Missing SIGHUP on Windows
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.