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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ jobs:
- name: Pyrefly type check
run: uv run --dev pyrefly check .

- name: Pyright type probe check
run: uv run --dev pyright typing_tests

- name: Mypy type check
run: uv run --dev mypy .

- name: Stubtest runtime typing check
run: uv run --dev stubtest alternative

- name: Run tests
run: |
# Some GitHub-hosted runners export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1.
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ The repository defines testing via GitHub actions. When contributing:
* `uv run --dev ruff format --check --diff .`
* `uv run --dev ruff check .`
* `uv run --dev pyrefly check .`
* `uv run --dev pyright typing_tests`
* `uv run --dev mypy .`
* `uv run --dev stubtest alternative`
* `uv run --dev pytest --verbosity=2 --cov=alternative --cov-report=xml --cov-fail-under=100 --junit-xml=test-results.xml`
* `uv run --group=docs sphinx-build --fail-on-warning --keep-going --builder=html docs /tmp/alternative-docs-html`
* Format code with `uv run --dev ruff format .` before committing.
* Keep the documentation in `docs/` up to date with user-facing behavior, API, and workflow changes. Documentation must compile without warnings.
* Keep `alternative.py` strictly typed: do not use `typing.Any` or `Any`, and do not add mypy or pyrefly suppression comments. Fix the annotations so public decorators remain transparent to type checkers and IDEs.
* For PyCharm-specific typing regressions, verify `typing_tests/type_probes.py` with `scripts/pycharm-type-probes.sh`. The script must produce no output when the probe file is clean.
* Any change to branching paths in `alternative.py` must be followed by a branch coverage run and review for material missing runtime coverage using `uv run --dev pytest --cov=alternative --cov-branch --cov-report=term-missing:skip-covered`.
* Name tests and functions in `snake_case` and give them triple-quoted docstrings similar to the current codebase.
69 changes: 64 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ When optimizing a hot path, it’s common to accumulate:

`alternative` keeps that workflow tidy by making implementation registration and selection first-class.

The same model works for module functions, instance methods, class methods, and static methods. Public typing is shipped in [`alternative.pyi`](alternative.pyi), so type checkers and IDEs can see the original call signatures instead of losing them behind the decorator objects.

## Quick example

```python
Expand Down Expand Up @@ -45,23 +47,72 @@ assert constant_number() == 2
assert unused_alternative_constant_number() == 3
```

See the [quickstart](https://alternative.readthedocs.io/en/latest/quickstart.html) for registration patterns, defaults, and method examples.

## Methods and descriptors

Decorate instance methods directly. For `@classmethod` and `@staticmethod`, put `@alternative.reference` and `.add(...)` outside the built-in descriptor decorator:

```python
import alternative


class Parser:
def __init__(self, value: str = ""):
self.value = value

@alternative.reference
def parse(self, value: str) -> int:
return int(value.strip())

@parse.add(default=True)
def parse_fast(self, value: str) -> int:
return int(value)

@alternative.reference
@classmethod
def from_text(cls, value: str) -> "Parser":
return cls(value.strip())

@from_text.add(default=True)
@classmethod
def from_text_fast(cls, value: str) -> "Parser":
return cls(value)

@alternative.reference
@staticmethod
def is_valid(value: str) -> bool:
return value.strip().isdigit()

@is_valid.add(default=True)
@staticmethod
def is_valid_fast(value: str) -> bool:
return value.isdigit()
```

Calling through an instance or class follows normal Python binding rules, and direct implementation calls bind the same way. The full descriptor examples are in [Use Methods](https://alternative.readthedocs.io/en/latest/quickstart.html#use-methods) and [Testing Methods](https://alternative.readthedocs.io/en/latest/pytest.html#testing-methods).

## Pytest features

The examples directory includes practical pytest patterns that make this library shine.
The pytest helpers are documented in the [pytest integration guide](https://alternative.readthedocs.io/en/latest/pytest.html).

### Pairwise equivalence checks

Use `pytest_parametrize_pairs(...)` to compare the reference against each candidate implementation.

- Basic pairwise checks: [`examples/test_measure.py`](examples/test_measure.py)
- More configurable pairwise checks: [`examples/test_equivalence.py`](examples/test_equivalence.py)
- [Equivalence Tests](https://alternative.readthedocs.io/en/latest/pytest.html#equivalence-tests)
- [Reference Caching](https://alternative.readthedocs.io/en/latest/pytest.html#reference-caching)

### Single-implementation parametrization

Use `pytest_parametrize(...)` to run one test body across all implementations.

- Great for benchmark workflows with [`pytest-benchmark`](https://pypi.org/project/pytest-benchmark/): [`examples/test_benchmark.py`](examples/test_benchmark.py)
- Useful for validating that every implementation passes one shared test suite
- [Only the Default Implementation](https://alternative.readthedocs.io/en/latest/pytest.html#only-the-default-implementation)
- [Benchmark All Implementations](https://alternative.readthedocs.io/en/latest/pytest.html#benchmark-all-implementations) with [`pytest-benchmark`](https://pypi.org/project/pytest-benchmark/)

## Runtime tools

`Alternatives.measure(...)` runs every implementation with the same arguments and measures the results with a callable you provide. See [Measure Implementations](https://alternative.readthedocs.io/en/latest/workflow.html#measure-implementations).

## Safety guarantees

Expand All @@ -75,3 +126,11 @@ The library tries to avoid unpleasant surprises caused by import order or accide
Set `ALTERNATIVE_DEBUG=1` to record where critical state changes happened (like selecting defaults or inspecting implementations). These locations are surfaced in error messages to make stateful issues easier to track down.

When debug mode is enabled, each `Implementation` also captures a label with its registration call-site. This label appears in `repr(...)` and selected debug errors, making it easier to disambiguate implementation instances.

## Typing and IDEs

`alternative` ships a top-level stub file, [`alternative.pyi`](alternative.pyi), for the public typing surface. It includes overloads for descriptor binding, transparent method/classmethod/staticmethod decoration, and the pytest helpers, while [`alternative.py`](alternative.py) stays focused on runtime behavior.

The typing probes are checked with mypy, pyright, pyrefly, and a headless PyCharm inspection script: [`scripts/pycharm-type-probes.sh`](scripts/pycharm-type-probes.sh). The PyCharm probe covers type assertions, unresolved references, and type checker warnings in [`typing_tests/type_probes.py`](typing_tests/type_probes.py).

Known PyCharm caveat: JetBrains `PyNestedDecoratorsInspection` currently reports a false-positive for correctly typed decorators stacked outside `@classmethod` or `@staticmethod`. Runtime behavior and type resolution are correct, and the project does not require `# noinspection PyTypeChecker` call-site suppressions for these examples.
Loading
Loading