Contributing¶
Guide for developers contributing to aria-testing.
Development Setup¶
Prerequisites¶
Installation¶
# Clone the repository
git clone https://github.com/t-strings/aria-testing.git
cd aria-testing
# Install dependencies and set up development environment
just install
# Verify setup
just ci-checks
Development Workflow¶
Using Just Recipes¶
The project uses just for all development tasks. Run just to see all available recipes:
just
Common recipes:
# Code quality
just lint # Check for linting issues
just lint-fix # Auto-fix linting issues
just fmt # Auto-format code
just fmt-check # Check formatting without changes
just typecheck # Run type checking
# Testing
just test # Run tests (sequential)
just test-parallel # Run tests (parallel with pytest-xdist)
just test-freethreaded # Run thread safety tests (8 threads x 10 iterations)
# Quality checks
just ci-checks # Run all checks (lint, format, typecheck, tests)
just ci-checks-ft # All checks + free-threading safety tests
# Documentation
just docs # Build documentation
# Benchmarking
just benchmark # Run performance benchmarks
just benchmark-cache # Run caching benchmarks
just profile-queries # Profile query operations
just profile-tests # Profile test suite
# Cleanup
just clean # Remove build artifacts and caches
Development Loop¶
Make your changes
Run quick checks:
just lintandjust typecheckRun tests:
just testRun full CI suite:
just ci-checksCommit when all checks pass
Code Standards¶
Modern Python (3.14+)¶
Use modern Python features:
Structural pattern matching:
match node:
case Element() as elem:
process_element(elem)
case Fragment() as frag:
process_fragment(frag)
case Text() as text:
process_text(text)
PEP 604 union syntax:
# ✅ Good
def query(container: Element | Fragment | Node) -> Element | None:
...
# ❌ Old style
from typing import Union, Optional
def query(container: Union[Element, Fragment, Node]) -> Optional[Element]:
...
Built-in generics:
# ✅ Good
def get_all(container: Element) -> list[Element]:
...
# ❌ Old style
from typing import List
def get_all(container: Element) -> List[Element]:
...
PEP 695 generic functions:
# ✅ Good
def make_query[T](finder: Callable[[T], list[Element]]) -> Query[T]:
...
# ❌ Old style
from typing import TypeVar, Generic
T = TypeVar('T')
def make_query(finder: Callable[[T], list[Element]]) -> Query[T]:
...
Type Hints¶
All code must have complete type hints:
# ✅ Good - complete type hints
def get_by_role(
container: Element | Fragment | Node,
role: str,
/,
*,
name: str | re.Pattern[str] | None = None,
level: int | None = None,
) -> Element:
...
# ❌ Bad - missing type hints
def get_by_role(container, role, name=None, level=None):
...
Code Style¶
Formatting: Use
ruff format(runs viajust fmt)Linting: Use
ruff check(runs viajust lint)Line length: 100 characters (configured in
pyproject.toml)Docstrings: Use Google-style docstrings for public APIs
Example docstring:
def get_by_role(
container: Element | Fragment | Node,
role: str,
/,
*,
name: str | re.Pattern[str] | None = None,
level: int | None = None,
) -> Element:
"""Find a single element by ARIA role.
Args:
container: Root element/fragment/node to search within.
role: ARIA role name (e.g., "button", "link", "heading").
name: Optional accessible name to match (string or regex).
level: Optional heading level (1-6, only for role="heading").
Returns:
The matching Element.
Raises:
ElementNotFoundError: When no elements match.
MultipleElementsError: When multiple elements match.
Examples:
>>> button = get_by_role(document, "button")
>>> heading = get_by_role(document, "heading", level=1)
>>> link = get_by_role(document, "link", name="Home")
"""
Testing Guidelines¶
Test Structure¶
Place tests in tests/ directory:
tests/
├── conftest.py # Shared fixtures
├── test_queries.py # Query function tests
├── test_cache.py # Cache system tests
├── test_errors.py # Error handling tests
└── test_utils.py # Utility function tests
Test Naming¶
Use descriptive names: test_<functionality>_<scenario>
# ✅ Good - clear what's being tested
def test_get_by_role_finds_button():
...
def test_get_by_role_raises_when_not_found():
...
def test_get_by_role_raises_when_multiple_found():
...
# ❌ Bad - unclear
def test_role():
...
def test_error():
...
Test Coverage¶
When adding features, test:
Happy path - Normal usage
Edge cases - Empty, None, unusual inputs
Error cases - Invalid inputs, not found, multiple found
Type handling - Different container types (Element, Fragment, Node)
Pattern matching - String and regex patterns
Example:
def test_get_by_text_exact_match():
"""Test exact text matching (happy path)."""
doc = html(t'<div><p>Hello World</p></div>')
element = get_by_text(doc, "Hello World")
assert element.tag == "p"
def test_get_by_text_pattern_match():
"""Test regex pattern matching (edge case)."""
doc = html(t'<div><p>Hello World</p></div>')
element = get_by_text(doc, re.compile(r"Hello.*"))
assert element.tag == "p"
def test_get_by_text_not_found():
"""Test error when text not found (error case)."""
doc = html(t'<div><p>Hello</p></div>')
with pytest.raises(ElementNotFoundError):
get_by_text(doc, "Goodbye")
def test_get_by_text_multiple_found():
"""Test error when multiple matches (error case)."""
doc = html(t'<div><p>Hello</p><p>Hello</p></div>')
with pytest.raises(MultipleElementsError):
get_by_text(doc, "Hello")
Running Tests¶
# Run all tests
just test
# Run specific test file
just test tests/test_queries.py
# Run specific test function
just test tests/test_queries.py::test_get_by_role_finds_button
# Run tests in parallel
just test-parallel
# Run with coverage
just test --cov
Thread Safety Testing¶
aria-testing is designed for Python 3.14’s free-threaded mode (no GIL). All code must be thread-safe.
Testing for Thread Safety¶
Use pytest-freethreaded to detect race conditions and threading issues:
# Run thread safety tests (8 threads, 10 iterations)
just test-freethreaded
# Custom thread/iteration counts
pytest --threads=16 --iterations=50 --require-gil-disabled tests/
# Test specific concurrency tests
pytest tests/test_concurrency.py -v
What Gets Tested¶
The --threads and --iterations options:
Run each test multiple times (
--iterations=10)Run tests concurrently across threads (
--threads=8)Expose race conditions, deadlocks, and non-deterministic behavior
Example:
# This test will run 80 times total (8 threads × 10 iterations)
pytest tests/test_queries.py::test_get_by_role --threads=8 --iterations=10
Writing Thread-Safe Tests¶
✅ Good - No shared mutable state:
def test_concurrent_queries():
# Each test creates its own container - thread-safe
doc = html(t'<div><button>Click</button></div>')
button = get_by_role(doc, "button")
assert button.tag == "button"
⚠️ Careful - Shared containers are OK if read-only:
# Module-level container (created once)
SAMPLE_DOC = html(t'<div><button>Click</button></div>')
def test_read_only_access():
# Read-only access to shared container - thread-safe
button = get_by_role(SAMPLE_DOC, "button")
assert button.tag == "button"
❌ Bad - Shared mutable state:
# Module-level mutable list - NOT thread-safe!
results = []
def test_with_shared_state():
# Multiple threads modifying same list - race condition!
element = get_by_role(doc, "button")
results.append(element) # ❌ NOT THREAD-SAFE
Thread Safety Guidelines¶
No global mutable state - Use function-local variables
Immutable data structures - Use
MappingProxyType, tuples, frozensetsNo caching without locks - Caching creates shared mutable state
Document thread-safety - Mark functions as thread-safe in docstrings
See tests/test_concurrency.py for examples of proper thread-safe testing.
Adding New Features¶
Adding a New Query Type¶
Implement the finder function:
# src/aria_testing/queries/by_custom.py
def _find_by_custom(
container: Element | Fragment | Node,
custom_attr: str,
) -> list[Element]:
"""Find all elements matching custom attribute."""
elements = get_all_elements(container)
return [
elem for elem in elements
if elem.attrs.get("data-custom") == custom_attr
]
Create query variants using factory:
from aria_testing.queries.factory import make_query_functions
# Generate all four variants
_custom_queries = make_query_functions(find_elements=_find_by_custom)
get_by_custom = _custom_queries.get_by
query_by_custom = _custom_queries.query_by
get_all_by_custom = _custom_queries.get_all
query_all_by_custom = _custom_queries.query_all
Export from main module:
# src/aria_testing/__init__.py
from aria_testing.queries.by_custom import (
get_by_custom,
query_by_custom,
get_all_by_custom,
query_all_by_custom,
)
Write tests:
# tests/test_custom.py
def test_get_by_custom_finds_element():
doc = html(t'<div data-custom="foo">Content</div>')
element = get_by_custom(doc, "foo")
assert element.tag == "div"
def test_get_by_custom_not_found():
doc = html(t'<div>Content</div>')
with pytest.raises(ElementNotFoundError):
get_by_custom(doc, "foo")
Add documentation in
docs/queries.mdRun checks:
just ci-checks
Adding a New ARIA Role¶
Update role mapping:
# src/aria_testing/roles/mapping.py
TAG_TO_ROLE = {
# ... existing mappings
"custom-element": "custom-role",
}
Update role computation if needed:
# src/aria_testing/roles/compute.py
def compute_role(element: Element) -> str | None:
match element.tag.lower():
case "custom-element":
# Custom logic for role computation
if element.attrs.get("type") == "special":
return "special-role"
return "custom-role"
case _:
return TAG_TO_ROLE.get(element.tag.lower())
Write tests:
# tests/test_roles.py
def test_custom_element_has_custom_role():
elem = html(t'<custom-element>Content</custom-element>')
role = compute_role(elem)
assert role == "custom-role"
Update documentation in
docs/queries.md(Supported Roles section)Run checks:
just ci-checks
Performance Considerations¶
Optimization Guidelines¶
Use caching - Add
@lru_cachefor expensive computationsEarly exit - Stop searching when you have enough results
Lazy evaluation - Defer expensive operations until needed
Iterative not recursive - Use explicit stacks for tree traversal
Set-based lookups - Use sets for O(1) membership checks
Benchmarking Changes¶
Always benchmark performance-critical changes:
# Run benchmarks before changes
just benchmark > before.txt
# Make your changes
# Run benchmarks after changes
just benchmark > after.txt
# Compare
diff before.txt after.txt
Profiling¶
Profile code to find bottlenecks:
# Profile query operations
just profile-queries
# Profile full test suite
just profile-tests
Documentation¶
Building Docs¶
# Build documentation
just docs
# View locally
open docs/_build/html/index.html
Documentation Structure¶
docs/index.md- Landing page (includes README)docs/queries.md- Query referencedocs/examples.md- Usage examplesdocs/api.md- API referencedocs/best-practices.md- Testing guidelinesdocs/architecture.md- Design documentationdocs/performance.md- Performance detailsdocs/contributing.md- This file
Writing Documentation¶
Use MyST Markdown syntax
Include code examples with syntax highlighting
Add type hints to function signatures
Link related sections with
[text](page.md)Use admonitions for notes, warnings, tips:
:::{note}
This is a note.
:::
:::{warning}
This is a warning.
:::
:::{tip}
This is a tip.
:::
Pull Request Process¶
Fork and clone the repository
Create a branch for your feature:
git checkout -b feature/my-featureMake changes following code standards
Write tests for your changes
Run checks:
just ci-checks(all must pass)Commit changes with clear messages
Push to your fork:
git push origin feature/my-featureOpen a pull request against the
mainbranch
PR Checklist¶
[ ] All tests pass (
just test)[ ] Type checking passes (
just typecheck)[ ] Linting passes (
just lint)[ ] Formatting is correct (
just fmt-check)[ ] Documentation is updated
[ ] Benchmarks show no regression (for performance changes)
[ ] CHANGELOG.md is updated (for user-facing changes)
Git Hooks¶
Enable pre-push hooks to run CI checks before pushing:
just enable-pre-push
This runs just ci-checks before every push, catching issues early.
To disable:
just disable-pre-push
Getting Help¶
Issues: GitHub Issues
Discussions: GitHub Discussions
Documentation: https://t-strings.github.io/aria-testing/
Code of Conduct¶
Be respectful, inclusive, and constructive. We’re all here to improve the library and help each other.
See Also¶
Architecture - System design and implementation
Performance - Performance optimization techniques
Best Practices - Testing guidelines