|
| 1 | +# AGENTS.md — infrared-protocols |
| 2 | + |
| 3 | +Guidelines for agentic coding agents working in this repository. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Project Overview |
| 8 | + |
| 9 | +`infrared-protocols` is a pure-Python library (Python ≥ 3.13) that encodes infrared |
| 10 | +remote-control protocols (e.g. NEC) into raw pulse/space timing sequences. The public |
| 11 | +API surface is small and intentionally minimal. |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## Environment Setup |
| 16 | + |
| 17 | +```bash |
| 18 | +# Bootstrap dev environment (uses uv) |
| 19 | +./script/setup.sh |
| 20 | + |
| 21 | +# Equivalent manual step |
| 22 | +uv pip install -e ".[dev]" |
| 23 | +``` |
| 24 | + |
| 25 | +Dev extras install: `pytest`, `prek` (lint/format/type-check runner). |
| 26 | + |
| 27 | +--- |
| 28 | + |
| 29 | +## Build / Lint / Test Commands |
| 30 | + |
| 31 | +### Run all lint, format, and type-check hooks (changed files only) |
| 32 | +```bash |
| 33 | +prek |
| 34 | +``` |
| 35 | + |
| 36 | +### Run all hooks on every file |
| 37 | +```bash |
| 38 | +prek --all-files |
| 39 | +``` |
| 40 | + |
| 41 | +### Run linter only (with auto-fix) |
| 42 | +```bash |
| 43 | +ruff check --fix infrared_protocols/ |
| 44 | +``` |
| 45 | + |
| 46 | +### Run formatter only |
| 47 | +```bash |
| 48 | +ruff format infrared_protocols/ |
| 49 | +``` |
| 50 | + |
| 51 | +### Run type checker only |
| 52 | +```bash |
| 53 | +basedpyright infrared_protocols/ |
| 54 | +``` |
| 55 | + |
| 56 | +### Run all tests |
| 57 | +```bash |
| 58 | +pytest --log-cli-level=debug |
| 59 | +``` |
| 60 | + |
| 61 | +### Run a single test file |
| 62 | +```bash |
| 63 | +pytest tests/test_commands.py |
| 64 | +``` |
| 65 | + |
| 66 | +### Run a single test by name |
| 67 | +```bash |
| 68 | +pytest tests/test_commands.py::test_nec_command_get_raw_timings_standard |
| 69 | +``` |
| 70 | + |
| 71 | +> CI tests against Python 3.13 and 3.14 via GitHub Actions. |
| 72 | +
|
| 73 | +--- |
| 74 | + |
| 75 | +## Repository Structure |
| 76 | + |
| 77 | +``` |
| 78 | +infrared_protocols/ # Library source (only this directory is linted/type-checked) |
| 79 | + __init__.py # Public API: defines __all__ and re-exports |
| 80 | + commands.py # All domain logic: Command ABC, NECCommand, Timing |
| 81 | +tests/ |
| 82 | + test_commands.py # pytest suite (all tests in one file) |
| 83 | +script/ |
| 84 | + setup.sh # Dev environment bootstrap |
| 85 | +.pre-commit-config.yaml # ruff, ruff-format, basedpyright hooks |
| 86 | +pyproject.toml # Build config, ruff rules, pyright settings |
| 87 | +``` |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## Code Style |
| 92 | + |
| 93 | +### Formatting |
| 94 | +- Enforced by `ruff-format` (Black-compatible). |
| 95 | +- **Indentation:** 4 spaces. |
| 96 | +- **Quotes:** Double quotes (`"..."`). |
| 97 | +- **Line length:** 88 characters (Black default). |
| 98 | +- **Trailing commas:** Required in multi-line collections and argument lists. |
| 99 | +- **Semicolons:** Never. |
| 100 | +- Two blank lines between top-level definitions; one blank line between methods. |
| 101 | + |
| 102 | +### Imports |
| 103 | +- **Within the package:** use relative imports (`from .commands import ...`). |
| 104 | +- **In tests:** use absolute imports from the installed package (`from infrared_protocols import ...`). |
| 105 | +- Named imports only; no wildcard imports (`from x import *`). |
| 106 | +- Import order is enforced by ruff's `I` (isort) rules: stdlib → third-party → local. |
| 107 | +- `__all__` must be defined in `__init__.py` to explicitly declare the public API. |
| 108 | + |
| 109 | +### Naming |
| 110 | +| Element | Convention | Example | |
| 111 | +|---|---|---| |
| 112 | +| Files / modules | `snake_case` | `commands.py` | |
| 113 | +| Packages | `snake_case` | `infrared_protocols` | |
| 114 | +| Classes | `PascalCase` | `NECCommand`, `Timing` | |
| 115 | +| Functions / methods | `snake_case` | `get_raw_timings` | |
| 116 | +| Variables / attributes | `snake_case` | `high_us`, `repeat_count` | |
| 117 | +| Local numeric constants | `snake_case` (not `UPPER_CASE`) | `leader_high = 9000` | |
| 118 | +| Test functions | `test_<subject>_<description>` | `test_nec_command_get_raw_timings_standard` | |
| 119 | +| Hex literals | Uppercase hex digits | `0xFF`, `0x04FB` | |
| 120 | + |
| 121 | +> Note: Local numeric constants intentionally use `snake_case` rather than |
| 122 | +> `SCREAMING_SNAKE_CASE`. Follow this convention throughout. |
| 123 | +
|
| 124 | +### Types |
| 125 | +- Type checker: `basedpyright` with `typeCheckingMode = "standard"`. |
| 126 | +- **All** function parameters and return types must be annotated. |
| 127 | +- `-> None` must be explicit on `__init__` and void methods. |
| 128 | +- Use PEP 585 lowercase generics: `list[Timing]`, not `List[Timing]`. |
| 129 | +- Use PEP 604 union syntax: `T | None`, not `Optional[T]`. |
| 130 | +- Use `@override` (from `typing`, Python 3.12+) on every overridden method. |
| 131 | +- No `Any`; avoid `cast`; prefer real type narrowing. |
| 132 | +- Inline variable annotations where needed: `timings: list[Timing] = []`. |
| 133 | + |
| 134 | +### Classes |
| 135 | +- Abstract base classes use `abc.ABC` and `@abc.abstractmethod`. |
| 136 | +- Immutable value objects use `@dataclass(frozen=True, slots=True)`. |
| 137 | +- Constructor arguments should be **keyword-only** (use `*` separator) to prevent |
| 138 | + positional-argument confusion. |
| 139 | + |
| 140 | +### Docstrings |
| 141 | +- All public classes and methods must have a docstring. |
| 142 | +- First line: concise one-line summary. |
| 143 | +- Multi-line: blank line after the summary, then prose description. No Google/NumPy |
| 144 | + parameter sections unless complexity demands it. |
| 145 | + |
| 146 | +```python |
| 147 | +def get_raw_timings(self) -> list[Timing]: |
| 148 | + """Get raw timings for the NEC command. |
| 149 | +
|
| 150 | + NEC protocol timing (in microseconds): |
| 151 | + - Leader pulse: 9000µs high, 4500µs low |
| 152 | + ... |
| 153 | + """ |
| 154 | +``` |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +## Architecture |
| 159 | + |
| 160 | +### Adding a New Protocol |
| 161 | +1. Subclass `Command` (ABC) in `infrared_protocols/commands.py`. |
| 162 | +2. Implement `get_raw_timings(self) -> list[Timing]`. |
| 163 | +3. Decorate the override with `@override`. |
| 164 | +4. Define timing constants as local `snake_case` variables inside the method. |
| 165 | +5. Re-export the new class from `infrared_protocols/__init__.py` and add it to |
| 166 | + `__all__`. |
| 167 | + |
| 168 | +### Key Abstractions |
| 169 | +- **`Timing(high_us, low_us)`** — frozen dataclass representing one pulse+space pair |
| 170 | + (microseconds). Immutable, comparable by value. |
| 171 | +- **`Command` (ABC)** — base class for all IR protocol encoders. Holds `modulation` |
| 172 | + and `repeat_count`. |
| 173 | +- **`NECCommand(Command)`** — encodes the NEC protocol; reference implementation. |
| 174 | + |
| 175 | +### Patterns to Follow |
| 176 | +- Build timing lists by starting with a base list, appending in a loop, then using |
| 177 | + `extend()` for repeat frames. |
| 178 | +- Repeat-code frame gaps replace the last timing's `low_us` (see `NECCommand`). |
| 179 | +- Bit manipulation uses masks like `data & 1`, `data >>= 1`, `(~x) & 0xFF`. |
| 180 | + |
| 181 | +--- |
| 182 | + |
| 183 | +## Testing |
| 184 | + |
| 185 | +- No mocking. Tests use pure value comparison against manually constructed |
| 186 | + `list[Timing]` fixtures. |
| 187 | +- One assertion per logical case; reuse expected values with list unpacking |
| 188 | + (`[*expected[:-1], ...]`) rather than duplicating fixtures. |
| 189 | +- Tests live in `tests/test_commands.py`; keep them in one file unless the suite |
| 190 | + grows substantially. |
| 191 | +- Pre-commit hooks (`prek`) do **not** run on `tests/`; the type checker and linter |
| 192 | + only target `infrared_protocols/`. Tests are still expected to be clean Python. |
| 193 | + |
| 194 | +--- |
| 195 | + |
| 196 | +## Error Handling |
| 197 | + |
| 198 | +- The library currently has no custom exceptions. Incorrect inputs surface as natural |
| 199 | + Python runtime errors (`TypeError`, `ValueError`). |
| 200 | +- Correctness is enforced primarily through the type checker and immutable value |
| 201 | + objects rather than defensive runtime checks. |
| 202 | +- If you add validation, raise standard built-in exceptions with descriptive messages |
| 203 | + rather than introducing custom exception classes unless there is a clear consumer |
| 204 | + need. |
0 commit comments