Thank you for considering contributing to Valid8r! This document provides guidelines and instructions for contributing to this project.
- Code of Conduct
- Fork-Based Contributions Required
- Getting Started
- Development Setup
- Development Workflow
- Code Style
- Security Guidelines
- Testing
- Commit Messages
- Pull Request Process
- Issue Reporting
- Documentation
- Community
This project adheres to the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to mikelane@gmail.com.
Important: This repository requires all contributions to be made via forks, not direct branches.
- External contributors: Must fork the repository and submit PRs from their fork
- Collaborators with write access: Should also use forks for contributions
- Why: This ensures a consistent workflow, improves security, and keeps the main repository clean
The main branch is protected and requires pull requests for all changes. Direct pushes to main are not allowed.
- Python 3.11 or higher (3.11-3.14 supported)
- uv for dependency management (10-100x faster than Poetry)
- pyenv (recommended for managing Python versions)
- Git
All contributions must come from a fork - you cannot create branches directly in the main repository.
-
Fork the repository on GitHub by clicking the "Fork" button at https://github.com/mikelane/valid8r
-
Clone your fork locally:
git clone https://github.com/YOUR-USERNAME/valid8r.git cd valid8r -
Add the upstream repository to keep your fork in sync:
git remote add upstream https://github.com/mikelane/valid8r.git
-
Verify your remotes:
git remote -v # You should see: # origin https://github.com/YOUR-USERNAME/valid8r.git (fetch) # origin https://github.com/YOUR-USERNAME/valid8r.git (push) # upstream https://github.com/mikelane/valid8r.git (fetch) # upstream https://github.com/mikelane/valid8r.git (push)
If using pyenv, install the required Python versions:
pyenv install 3.14.0
pyenv install 3.13.9
pyenv install 3.12.12
pyenv install 3.11.14
pyenv local 3.14.0 3.13.9 3.12.12 3.11.14curl -LsSf https://astral.sh/uv/install.sh | shVerify installation:
uv --version
# Should show: uv 0.9.x or lateruv syncThis installs all dependencies including dev, test, lint, and docs groups.
Note: If you previously used Poetry, see docs/migration-poetry-to-uv.md for migration instructions.
uv run pre-commit installPre-commit hooks automatically:
- Trim trailing whitespace
- Fix end of files
- Check YAML and TOML syntax
- Run ruff (linting and formatting)
- Run mypy (type checking)
Before creating a new feature branch, sync your fork with the upstream repository:
# Fetch upstream changes
git fetch upstream
# Update your local main branch
git checkout main
git merge upstream/main
# Push updates to your fork
git push origin mainImportant: Create branches in your fork, not in the upstream repository.
# Ensure you're on an updated main branch
git checkout main
git pull upstream main
# Create and switch to a new feature branch
git checkout -b feat/your-feature-name
# or
git checkout -b fix/your-bug-fixWrite clean, well-tested code following our style guidelines.
# Run all tests with coverage
uv run tox
# Run only unit tests
uv run pytest tests/unit
# Run BDD tests
uv run tox -e bdd
# Run linting
uv run tox -e lintUse Conventional Commits format:
git add .
git commit -m "feat: add new email validation parser"See Commit Messages section for details.
Push to your fork (origin), not upstream:
# Push your feature branch to YOUR fork
git push origin feat/your-feature-nameThen create a Pull Request on GitHub:
- Go to https://github.com/mikelane/valid8r
- Click "Pull requests" → "New pull request"
- Click "compare across forks"
- Select your fork and branch as the source
- Fill out the PR template completely
- Line length: 120 characters
- Quotes: Single quotes for strings, double quotes for docstrings
- Type hints: All functions must be fully type-annotated
- Imports: Sorted using isort (automatic via pre-commit)
# Format code with ruff
uv run ruff format .
# Check and fix linting issues
uv run ruff check . --fix
# Check type hints
uv run mypy valid8r- Functional composition over imperative validation
- Maybe monad pattern for error handling (Success/Failure)
- SOLID principles in design
- No external dependencies in core library (except uuid-utils)
- Parse and validate in a single pipeline using
bindandmap - Keep core logic free of I/O and side effects
- Use dependency injection for testing
- Mirror test structure to source structure
When adding or modifying parsers, follow these security requirements:
Always validate input length BEFORE expensive operations (regex, parsing, computation):
def parse_foo(text: str) -> Maybe[Foo]:
"""Parse foo with DoS protection."""
if not text or not isinstance(text, str):
return Maybe.failure('Input required')
s = text.strip()
if s == '':
return Maybe.failure('Input cannot be empty')
# CRITICAL: Early length guard (DoS mitigation)
if len(text) > MAX_LENGTH:
return Maybe.failure('Input too long')
# Now safe to perform expensive operations (regex, etc.)
# ...Rationale: Prevents DoS attacks where attackers send extremely large inputs to consume server resources.
Example: Without length guard, parsing 1MB input takes ~48ms. With length guard, rejection takes <1ms.
Regular expressions can be vulnerable to catastrophic backtracking:
❌ Avoid nested quantifiers:
# BAD: Nested quantifiers cause exponential time complexity
pattern = r'(a+)+'
pattern = r'(a*)*'
pattern = r'(a|ab)+'✅ Use atomic patterns:
# GOOD: Linear time complexity
pattern = r'a+'
pattern = r'(?:a|ab)+' # Non-capturing groupTools to use:
- Run
regexploiton all regex patterns - Test with pathological inputs (repeated characters)
- Consider using
regexmodule with timeout
Every parser must have a DoS prevention test:
def it_rejects_excessively_long_input(self) -> None:
"""Reject extremely long input to prevent DoS attacks."""
import time
malicious_input = 'x' * 1_000_000 # 1MB input
start = time.perf_counter()
result = parse_foo(malicious_input)
elapsed_ms = (time.perf_counter() - start) * 1000
# Verify both correctness AND performance
assert result.is_failure()
assert 'too long' in result.error_or('').lower()
assert elapsed_ms < 10, f'Rejection took {elapsed_ms:.2f}ms, should be < 10ms'Include security information in docstrings:
def parse_foo(text: str) -> Maybe[Foo]:
"""Parse foo with security protection.
Args:
text: Input string to parse
Returns:
Success containing Foo if valid, Failure with error message otherwise
Security:
- Maximum input length: 100 characters
- Time complexity: O(n)
- ReDoS-safe regex patterns
Examples:
>>> result = parse_foo("valid-input")
>>> assert result.is_success()
"""All parsers must pass security tests before merging:
# Run security test suite (DoS and ReDoS protection)
uv run pytest tests/security/ -v
# Run ReDoS scanner on codebase
uv run python scripts/check_regex_safety.py valid8r/
# Run full security environment (tests + scanner)
uv run tox -e securityThis project includes automated Regular Expression Denial of Service (ReDoS) detection that runs in CI/CD:
- Scanner Tool:
scripts/check_regex_safety.py(powered by regexploit) - CI Workflow:
.github/workflows/security-checks.yml - When It Runs: On every push and pull request
- What It Does: Scans all regex patterns for catastrophic backtracking vulnerabilities
Example scan output:
$ uv run python scripts/check_regex_safety.py valid8r/core/parsers.py
✅ All 4 regex pattern(s) are safe (no ReDoS vulnerabilities detected)If vulnerabilities are found, the PR will be blocked until they are fixed.
DO NOT open public issues for security vulnerabilities.
See SECURITY.md for vulnerability reporting process.
Before submitting a PR that adds/modifies parsers:
- Input length validated BEFORE expensive operations
- All regex patterns analyzed for ReDoS vulnerabilities
- ReDoS scanner passes:
uv run python scripts/check_regex_safety.py valid8r/ - DoS regression test added with performance assertion (< 10ms for oversized input)
- Maximum input length documented in docstring
- Security tests pass locally:
uv run pytest tests/security/ - No sensitive data (API keys, emails, etc.) in code/tests
See Secure Parser Development Guide for comprehensive guidelines.
- Every new feature must have tests
- Every bug fix must have a test that would have caught the bug
- Maintain or improve code coverage (currently 100%)
tests/
├── unit/ # Unit tests mirroring source structure
├── bdd/ # BDD/Cucumber tests
│ ├── features/ # Gherkin feature files
│ └── steps/ # Step definitions
└── integration/ # Integration tests
- Test files:
test_*.py - Test classes:
Describe[ClassName](e.g.,DescribeParseInt) - Test methods:
it_[describes_behavior](e.g.,it_parses_positive_integers)
from valid8r import parsers
from valid8r.testing import assert_maybe_success, assert_maybe_failure
class DescribeParseEmail:
"""Tests for parse_email function."""
def it_parses_valid_email(self):
"""It parses a valid email address."""
result = parsers.parse_email("user@example.com")
assert assert_maybe_success(result)
assert result.value_or(None).domain == "example.com"
def it_rejects_invalid_email(self):
"""It rejects an invalid email address."""
result = parsers.parse_email("not-an-email")
assert assert_maybe_failure(result, "valid email")- Test behavior, not implementation: Assert on public API only
- Small and hermetic: No network calls, use tmp_path, inject time
- Deterministic: Seed randomness, avoid sleeps
- DAMP not DRY: Prefer clarity over reuse in tests
- One concept per test: Each test fails for one clear reason
Use @pytest.mark.parametrize with clear IDs:
@pytest.mark.parametrize(
"raw,expected",
[
pytest.param("42", 42, id="positive"),
pytest.param("0", 0, id="zero"),
pytest.param("-1", -1, id="negative"),
],
)
def it_parses_integers(raw, expected):
result = parsers.parse_int(raw)
assert result.value_or(None) == expectedWe use Conventional Commits which enable automatic semantic versioning and changelog generation.
<type>(<scope>): <subject>
<body>
<footer>
feat: New feature (triggers MINOR version bump)fix: Bug fix (triggers PATCH version bump)docs: Documentation only changesstyle: Code style changes (formatting, missing semi-colons, etc)refactor: Code refactoring (no functional changes)perf: Performance improvements (triggers PATCH version bump)test: Adding or updating testsbuild: Build system or external dependency changesci: CI configuration changeschore: Other changes that don't modify src or test files
For breaking changes, add BREAKING CHANGE: in the footer or use ! after the type:
feat!: remove deprecated parse_phone function
BREAKING CHANGE: parse_phone has been removed in favor of parse_phone_number
This triggers a MAJOR version bump.
# Feature addition (minor version bump)
feat: add parse_uuid function with version validation
# Bug fix (patch version bump)
fix: correct email domain normalization case handling
# Performance improvement (patch version bump)
perf: optimize parse_int for large numbers
# Breaking change (major version bump)
feat!: change Maybe.bind signature to accept keyword arguments
BREAKING CHANGE: Maybe.bind now requires parser functions to accept
keyword arguments instead of positional arguments.
# Documentation update (no version bump)
docs: add examples for parse_url structured results
# Multiple scopes
feat(parsers,validators): add phone number validationSee .github/CONVENTIONAL_COMMITS.md for more examples.
- Ensure all tests pass:
uv run tox - Update documentation if needed
- Add changelog entry if applicable
- Verify your commits follow the conventional commit format
Fill out the PR template completely, including:
- Summary of changes
- Motivation and context
- Type of change
- Test coverage
- Code quality checklist
- Documentation updates
- Automated checks must pass (CI, linting, tests)
- Code review by maintainers
- Address feedback and update as needed
- Approval required before merge
Once your PR is merged to main, the automated release process begins:
- Semantic Analysis: The semantic-release workflow analyzes all commits since the last release
- Version Calculation: Determines version bump based on commit types:
feat:commits → MINOR version bump (0.X.0)fix:,perf:commits → PATCH version bump (0.0.X)feat!:orBREAKING CHANGE:→ MAJOR version bump (X.0.0)docs:,chore:,ci:,refactor:,test:→ No version bump
- If version bump is needed:
- Version in
pyproject.tomlis updated CHANGELOG.mdis auto-generated from commit messages- Git tag is created (e.g.,
v0.9.1) - Package is built with
uv build - Published to PyPI via trusted publishing
- Version in
- If no version bump is needed:
- Workflow succeeds with no changes
- No tag, no publish
Monitoring Your Release:
# Watch workflow execution
gh run list --workflow=semantic-release.yml --limit 5
# Check if version was bumped
gh release list
# Verify PyPI publication
pip index versions valid8rTypical Timeline:
- Merge to main: Immediate
- Semantic-release workflow starts: ~10 seconds
- Build and publish: ~60-90 seconds
- Available on PyPI: ~2-3 minutes total
Use the bug report template and include:
- Python version
- Valid8r version
- Minimal reproduction example
- Expected vs actual behavior
- Stack trace if applicable
Use the feature request template and include:
- Clear description of the feature
- Use cases and motivation
- Proposed API (if applicable)
- Alternative solutions considered
Use GitHub Discussions instead of issues for:
- "How do I...?" questions
- Feature ideas and RFC-style proposals
- General discussion about validation patterns
- Sharing projects built with Valid8r
Visit GitHub Discussions:
- Q&A - Ask for help
- Ideas - Suggest features
- Show and Tell - Share your projects
- Announcements - Updates from maintainers
Use GitHub Issues for:
- Bug reports
- Feature requests with technical specifications
- Documentation errors
Use Google or NumPy style docstrings:
def parse_email(text: str) -> Maybe[EmailAddress]:
"""Parse and normalize an email address.
Args:
text: The email address string to parse.
Returns:
Success containing EmailAddress if valid, Failure with error message otherwise.
Examples:
>>> result = parse_email("User@Example.COM")
>>> assert result.is_success()
>>> email = result.value_or(None)
>>> assert email.domain == "example.com" # normalized to lowercase- All public functions must have comprehensive docstrings
- Include type hints (handled automatically)
- Provide usage examples as doctests
- Document error cases and edge cases
# Build documentation
uv run docs-build
# Serve with live reload
uv run docs-serveView at http://localhost:8000
- GitHub Discussions: Visit Discussions for questions, ideas, and community support
- GitHub Issues: For bugs and feature requests with technical specifications
- Documentation: https://valid8r.readthedocs.io/
See the Welcome Discussion for community guidelines and tips on when to use Discussions vs Issues.
- Watch Announcements for release updates
- Follow the repository for releases
- Read the CHANGELOG.md
# Run smoke test
uv run python smoke_test.py
# Check coverage
uv run pytest --cov=valid8r --cov-report=html tests/unit
# View at htmlcov/index.html
# Run specific test
uv run pytest tests/unit/test_parsers.py::DescribeParseInt::it_parses_positive_integers
# Watch mode (requires pytest-watch)
uv run ptw tests/unit- Use
breakpoint()for debugging (built-in Python 3.7+) - pytest --pdb to drop into debugger on failures
- Use MockInputContext for testing prompts
import timeit
setup = "from valid8r import parsers"
stmt = "parsers.parse_int('12345')"
time = timeit.timeit(stmt, setup=setup, number=100000)
print(f"Average time: {time/100000*1000000:.2f} μs")We value every contribution and recognize contributors at different levels. See CONTRIBUTORS.md for the full list.
| Tier | Requirements | Recognition |
|---|---|---|
| Explorer | 1st merged PR | Welcome mention in release notes |
| Builder | 3+ merged PRs | Listed in CONTRIBUTORS.md |
| Champion | 10+ merged PRs | Triage permissions, badge in profile |
| Maintainer | 25+ merged PRs + consistent quality | Review permissions, co-maintainer status |
- Code: Features, bug fixes, performance improvements
- Documentation: Guides, API docs, examples
- Tests: Unit tests, BDD scenarios, integration tests
- Infrastructure: CI/CD, tooling, developer experience
- Community: Answering questions, reviewing PRs, triaging issues
- Security: Vulnerability reports, security improvements
- First PR merged: Welcome message in the PR
- 10th PR merged: Shoutout in release notes
- 25th PR merged: Featured in project announcements
New to Valid8r? Look for issues labeled good first issue. These are:
- Well-documented with clear requirements
- Scoped to a single, focused change
- Include hints on where to start
- Have a maintainer available to help
We also label help wanted issues that are ready for community contribution.
By contributing to Valid8r, you agree that your contributions will be licensed under the MIT License.
Don't hesitate to ask questions! File an issue or start a discussion on GitHub.
Thank you for contributing to Valid8r!