Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: CI

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

env:
PYTHONDONTWRITEBYTECODE: "1"
PYTHONUNBUFFERED: "1"

jobs:
ruff-format:
name: Check Code Formatting (Ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install ruff>=0.8.0

- name: Check formatting
run: ruff format --check --diff pysdl/ examples/ tests/

ruff-lint:
name: Lint Code (Ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install ruff>=0.8.0

- name: Run linter
run: ruff check pysdl/ examples/ tests/

mypy-type-check:
name: Type Check (mypy)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install mypy==1.13.0

- name: Run type checker
run: mypy pysdl/ examples/ --ignore-missing-imports

pytest:
name: Run Tests (pytest)
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'

- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -e ".[dev]"

- name: Run tests with coverage
run: |
pytest --cov=pysdl --cov-report=term --cov-report=xml --cov-report=html --tb=short -v

- name: Upload coverage reports to Codecov
if: matrix.python-version == '3.13'
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

- name: Upload coverage artifacts
if: matrix.python-version == '3.13'
uses: actions/upload-artifact@v4
with:
name: coverage-reports
path: |
htmlcov/
coverage.xml
retention-days: 30
81 changes: 81 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Publish to PyPI

on:
release:
types: [published]
workflow_dispatch: # Allow manual triggering

permissions:
contents: read

jobs:
build:
name: Build distribution
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build

- name: Build package
run: python -m build

- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/

publish-to-pypi:
name: Publish to PyPI
if: github.event_name == 'release' && github.event.action == 'published'
needs: [build]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/pysdl
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing

steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

publish-to-testpypi:
name: Publish to TestPyPI
if: github.event_name == 'workflow_dispatch'
needs: [build]
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/p/pysdl

permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing

steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish distribution to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,11 @@ async_sdl_python/
- **Python**: 3.9 or higher (uses type hints and async features)
- **Dependencies**: None (uses only Python standard library)
- **Development Dependencies**:
- pytest >= 7.0
- pytest-asyncio >= 0.21
- pytest-cov >= 4.0
- mypy >= 1.0 (for type checking)
- pylint >= 2.0 (for linting)
- pytest >= 8.0
- pytest-asyncio >= 0.23
- pytest-cov >= 4.1
- mypy >= 1.13 (for type checking)
- ruff >= 0.8 (for linting)

## Testing

Expand All @@ -199,7 +199,7 @@ pytest --cov=pysdl --cov-report=html
mypy pysdl/

# Run linting
pylint pysdl/
ruff check pysdl/
```

## Contributing
Expand All @@ -220,7 +220,7 @@ Contributions are welcome! Please:
- **Type Safety**: All code must have comprehensive type hints
- **Testing**: Maintain >90% test coverage
- **Documentation**: Update docs for API changes
- **Code Style**: Follow PEP 8, use black formatter
- **Code Style**: Follow PEP 8, use ruff formatter
- **Async**: Use async/await properly, avoid blocking calls
- **Logging**: Use SdlLogger for framework events

Expand Down Expand Up @@ -248,8 +248,6 @@ This project is licensed under the MIT License - see the LICENSE file for detail

PySDL v1.0.0 introduces breaking changes to support instance-based systems. This enables running multiple independent SDL systems in the same process and eliminates global state.

**See [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md) for detailed migration instructions from v0.0.1 to v1.0.0.**

Key changes:
- `SdlSystem` is now instance-based - create instances with `system = SdlSystem()`
- `SdlProcess.create()` and `__init__()` now require `system` parameter
Expand All @@ -269,15 +267,14 @@ Future enhancements under consideration:

## Version History

- **1.0.0** (2025-10-26) - Instance-based system **[BREAKING CHANGES]**
- **1.0.0** (2024-10-26) - Instance-based system **[BREAKING CHANGES]**
- Refactored `SdlSystem` from static class to instance-based
- Added `system` parameter to `SdlProcess` creation and initialization
- Processes now reference their system instance via `self._system`
- Enables multiple independent systems in the same process
- Eliminates global state for better testability
- All tests updated (242 tests, 83% coverage)
- All examples updated to use instance-based API
- See [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md) for upgrade path

- **0.0.1** - Initial release
- Core actor model implementation
Expand Down
27 changes: 24 additions & 3 deletions docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ Register a process with the system.
**Returns:**
- `True` if registered successfully, `False` if process is None or already registered

**Raises:**
- `ValidationError`: If process is None or has invalid PID

**Example:**
```python
# Usually called automatically by Process.create()
Expand All @@ -97,7 +100,10 @@ Unregister a process and clean up its resources.
- `process`: Process instance to unregister

**Returns:**
- `True` if unregistered successfully, `False` if process is None
- `True` (always)

**Raises:**
- `ValidationError`: If process is None or has invalid PID

**Side Effects:**
- Removes process from `proc_map`
Expand All @@ -119,6 +125,10 @@ Add a signal to the system queue.
**Parameters:**
- `signal`: Signal to enqueue

**Raises:**
- `ValidationError`: If signal is None or invalid
- `QueueError`: If queue operation fails

**Example:**
```python
# Usually called by process.input()
Expand All @@ -137,6 +147,10 @@ Route a signal to its destination process.
**Returns:**
- `True` if delivered successfully, `False` otherwise

**Raises:**
- `ValidationError`: If signal is None or has no destination
- `SignalDeliveryError`: If signal delivery to destination fails

**Side Effects:**
- Adds destination process to `ready_list`
- Enqueues signal to process inbox
Expand All @@ -159,6 +173,10 @@ Start a timer for a process.
**Parameters:**
- `timer`: Timer to start

**Raises:**
- `ValidationError`: If timer is None
- `TimerError`: If timer has no source PID

**Side Effects:**
- Stops timer if already running (prevents duplicates)
- Adds timer to `timer_map[pid]`
Expand All @@ -184,6 +202,9 @@ Stop a specific timer.
**Returns:**
- `True` if timer was found and stopped, `False` otherwise

**Raises:**
- `ValidationError`: If timer is None

**Side Effects:**
- Removes timer from `timer_map[pid]`
- Deletes PID entry if no timers remain
Expand All @@ -201,7 +222,7 @@ system.stopTimer(timer)
Run the main event loop.

**Returns:**
- Never returns normally (runs until stopped)
- `True` when stopped normally

**Behavior:**
- Processes signals from queue
Expand Down Expand Up @@ -1018,7 +1039,7 @@ Find a handler for a state/event combination using priority-based wildcard match
handler = fsm.find(state_running, WorkSignal.id())

# Star state match (Priority 2)
handler = fsm.find(state_any, EmergencySignal.id()) # If star handler registered
handler = fsm.find(star, EmergencySignal.id()) # If star handler registered

# Star signal match (Priority 3)
handler = fsm.find(state_init, unknown_signal.id()) # If star signal handler registered
Expand Down
Loading