Thank you for your interest in contributing to throttled-py! This guide will help you get started and ensure a smooth review process. If anything is unclear, please feel free to open an issue or ask in your pull request.
- Getting Started
- Development Setup
- Code Style
- Testing Guidelines
- Documentation
- Public vs Internal APIs
- Commit and Branch Conventions
- Pull Request Process
- PR Checklist
- Fork the repository and clone your fork.
- Read
AGENTS.md, then read thisCONTRIBUTING.md. - Create a branch from
mainfor your changes. - Follow the Development Setup to configure your environment.
- Make your changes, following the guidelines below.
- Submit a pull request.
- Python 3.10+
- uv package manager
uv sync --group allThis project uses prek to run lint and format checks automatically before each commit.
# Install git hooks (first time only)
uv run prek installVerify the installation:
# Should list: check-merge-conflict, ruff, ruff-format, mypy-strict
uv run prek listYou can also run the hooks manually:
# Run on specific files
uv run prek run --files <changed-files>
# Run on all files
uv run prek run --all-filesImportant: Please run prek locally before pushing. CI will block merging if the Code Quality check fails.
All code is linted and formatted by ruff.
Use pyproject.toml as the source of truth for:
- line length
- enabled and ignored rule families
- per-file ignores
- formatting options
Read tool.ruff and tool.ruff.lint before changing lint behavior.
Docstring style is covered in Docstrings.
Please add type annotations to all variables. Use Python 3.10+ built-in generics:
# Good
result: list[str] = []
data: dict[str, int] = {}
# Bad - deprecated typing generics
from typing import List, Dict
result: List[str] = []- Do not add
from __future__ import annotations. - Use explicit string forward references when an annotation needs a name that is unavailable at runtime.
- Move imports used only by annotations into
if TYPE_CHECKING:because RuffTCHenforces this. - Choose quote scope by runtime availability: use partial quotes when only the inner name is missing (
type["BaseRateLimiter"]), and quote the whole expression when the outer name is also missing ("ClassVar[dict[str, type[BaseRateLimiter]]]").
Use reStructuredText (rst) style because Sphinx autodoc parses rst fields from docstrings.
Docstring requirements:
- Use rst field syntax, such as
:param:and:return:. - Avoid
Args:andReturns:sections; they do not render correctly in generated docs.
# Good - rst style (parsed correctly by Sphinx autodoc)
def build_hook_chain(
hooks: list[Hook],
do_limit: Callable[[], "RateLimitResult"],
context: HookContext,
) -> Callable[[], "RateLimitResult"]:
"""Build a hook chain using middleware pattern.
:param hooks: List of hooks to chain.
:param do_limit: The actual rate limit function to be wrapped.
:param context: The hook context containing rate limit metadata.
:return: A callable that executes the hook chain.
"""
# Bad - Args/Returns sections (will NOT render in Sphinx API docs)
def build_hook_chain(...):
"""Build a hook chain.
Args:
hooks: List of hooks to chain.
Returns:
A callable.
"""Please write all tests as class-based with the @classmethod decorator. Standalone test functions are not accepted.
# Good
class TestBuildHookChain:
@classmethod
def test_on_limit__multi_hooks(
cls, hook_context: HookContext, rate_limit_result: RateLimitResult
) -> None:
"""Multiple hooks should execute in correct order."""
...
# Bad
def test_build_hook_chain_multi_hooks():
...Please use the format test_{function_name}__{case_description} with double underscores separating the target and the case:
class TestOTelHook:
def test_allowed_request__records_metrics(cls, ...): ...
def test_denied_request__records_metrics(cls, ...): ...
def test_custom_cost__recorded(cls, ...): ...Please use mocks for external dependencies (e.g., OpenTelemetry SDK). Understanding the request-response interface and mocking it accordingly keeps tests simple:
@pytest.fixture
def mock_meter() -> MagicMock:
meter: MagicMock = MagicMock(name="Meter")
meter.create_counter.return_value = MagicMock(name="Counter")
meter.create_histogram.return_value = MagicMock(name="Histogram")
return meter- No duplicate fixtures: Please share common fixtures via
conftest.py. - No empty files: Please remove empty
conftest.pyfiles. - Benchmarks: Please add
@pytest.mark.skip(reason="skip benchmarks")to benchmark test classes.
- Unit test mirroring: tests for
throttled/<package>/<module>.pyshould be placed attests/<package>/test_<module>.py. - Integration tests: tests validating class behavior through public APIs should stay with that class's integration test file (for example,
tests/test_throttled.py).
CI reports coverage through Codecov.
Use codecov.yml as the source of truth for:
- project coverage gates
- patch coverage gates
You can check coverage locally:
uv run pytest -n auto --cov=throttled --cov-report=term-missing tests/# Run all tests
uv run pytest -n auto tests/ -x
# Run a specific test file
uv run pytest -n auto tests/test_hooks.py -vThis project uses Sphinx with autodoc to generate API documentation from docstrings. If you're adding or modifying public APIs, please make sure the docs build correctly:
# Build HTML docs (sphinx is included in `uv sync --group all`)
cd docs
uv run make html
# Preview locally at http://localhost:8000
python -m http.server 8000 --directory build/htmlThe generated docs will be in docs/build/html/. Please use rst-style docstrings (:param:, :return:) so Sphinx can parse them correctly — see Docstrings for details.
When you add or change a public API, update documentation in the same PR:
README.mdandREADME_ZH.md- relevant
docs/source/pages - runnable examples in
examples/(sync + async when applicable)
New API forms must be additive in docs. Keep existing API forms documented unless explicitly deprecated.
When introducing a user-facing API:
- Export through the package
__init__.pychain (keep sync/async symmetry when relevant). - Re-export at
throttled/__init__.pywhen it should be top-level user-facing. - Prefer tests importing from public paths.
- Update docs and examples in the same PR (see Documentation).
For internal-only modules/utilities:
- Do not export from
__init__.pyor__all__. - Unit tests may import internal modules directly.
- Integration tests should still validate behavior through public APIs.
Create branches from main and use this format:
<type>/<yymmdd>_<topic>
Branch components:
type: change category. Usefeat,fix,docs,ci,refactor,test,chore,perf, orbuild.yymmdd: date with a two-digit year, for example260405topic: short lowercase identifier separated with_
Examples:
git checkout main
git pull --ff-only upstream main
git checkout -b feat/260405_quota_parser_dsl
git checkout -b docs/260405_contributing_branch_rulesCommit conventions:
- Follow Conventional Commits.
- CI validates commit messages with commitlint.
<type>: <description>
Rules:
- Append
(#<issue-number>)only when the commit intentionally links to a specific issue. - Use a scope when useful, for example
docs(contributing): shorten ruff guidance.
feat: a new featurefix: a bug fixdocs: documentation-only changesci: CI configuration changesrefactor: code changes that neither fix a bug nor add a featuretest: adding or updating testschore: build process or tooling changesperf: performance improvementsbuild: build system or dependency changes
# Good
git commit -m "refactor: narrow memory store lock typing"
git commit -m "feat: add hook system with OpenTelemetry support (#37)"
git commit -m "fix: add @wraps decorator for metadata preservation (#120)"
git commit -m "docs: update contributing guidelines (#127)"
# Bad - will fail CI
git commit -m "updated hooks" # missing type
git commit -m "Feat: add hooks" # uppercase type
git commit -m "feat : add hooks" # space before colonRebase onto the latest main before submitting:
# Option A: if you have an upstream remote
git fetch upstream main
git rebase upstream/main
# Option B: if you only have origin
git fetch origin main
git rebase origin/mainDo not use merge commits to sync with main (for example, git merge main).
Push your branch normally. You do not need to squash commits before pushing; the maintainer will squash-merge the PR when it is ready.
git push origin <your-branch>If you rebased onto main, you may need to force-push:
git push --force-with-lease origin <your-branch>Use the canonical review documents:
.github/copilot-instructions.md: project-specific code review guidanceCONTRIBUTING.md: contributor workflow and project conventions
- Please address all review comments before requesting re-review.
- Rebase onto
mainwhen the maintainer asks you to sync. - Please re-run prek after making changes to ensure lint checks still pass.
Current CI definitions live in .github/workflows/.
Before merging, the workflows relevant to the PR must pass, including:
- code quality
- tests
- commit message validation
- coverage reporting
- triggered release workflows
Before submitting your pull request, please verify the following:
-
uv run prek run --all-filespasses locally -
uv run pytest -n auto tests/ -xpasses locally
- Type annotations added to all variables
- Docstrings use reStructuredText style (
:param:,:return:) - Tests are class-based with
@classmethod - Test names follow
test_{func}__{case}format - Unit tests follow test mirroring path rules (
throttled/<package>/<module>.py->tests/<package>/test_<module>.py) - No empty files or unused code
- No duplicate fixtures
- Public API changes include README (EN + ZH), docs, and examples updates
- Commit message follows Conventional Commits (
<type>: <description>) - Branch name follows
<type>/<yymmdd>_<topic> - Branch is rebased onto
main