wry (Why Repeat Yourself?) is a Python library that combines the power of Pydantic models with Click CLI framework, enabling you to define your CLI arguments and options in one place using type annotations. Following the DRY (Don't Repeat Yourself) principle, it eliminates the repetition of defining arguments, types, and validation rules separately.
- π― Single Source of Truth: Define your CLI structure using Pydantic models with type annotations
- π Type Safety: Full type checking and validation using Pydantic
- π Multiple Input Sources: Automatically handles CLI arguments, environment variables, and config files
- π Value Source Tracking: Know whether each config value came from CLI, env, config file, or defaults
- π¨ Auto-Generated CLI: Automatically generates Click options and arguments from your Pydantic models
- π Rich Help Text: Auto-generated help includes type information, constraints, and defaults
- π§ Validation: Leverage Pydantic's validation system with helpful error messages
- π³ Environment Variable Support: Automatic env var discovery with customizable prefixes
- π Config File Support: Load configuration from JSON files with proper precedence
pip install wryThe simplest way to use wry is with AutoWryModel, which automatically generates CLI options for all fields:
import click
from pydantic import Field
from wry import AutoWryModel
class AppArgs(AutoWryModel):
"""Configuration for my app."""
name: str = Field(description="Your name")
age: int = Field(default=25, ge=0, le=120, description="Your age")
verbose: bool = Field(default=False, description="Verbose output")
@click.command()
@AppArgs.generate_click_parameters()
def main(**kwargs: Any):
"""My simple CLI application."""
# Create the model instance from kwargs
config = AppArgs(**kwargs)
click.echo(f"Hello {config.name}, you are {config.age} years old!")
if __name__ == "__main__":
main()See comprehensive examples:
examples/autowrymodel_comprehensive.py- All AutoWryModel features including aliasesexamples/wrymodel_comprehensive.py- WryModel with source trackingexamples/multimodel_comprehensive.py- Multi-model usage
Run it:
$ python app.py --name Alice --age 30 --verbose
Hello Alice, you are 30 years old!
Name was provided via: ValueSource.CLI
# Also supports environment variables
$ export WRY_NAME=Bob
$ python app.py --age 35
Hello Bob, you are 35 years old!wry tracks where each configuration value came from, supporting all four sources:
- DEFAULT: Values from model field defaults
- ENV: Values from environment variables
- JSON: Values from configuration files (via
--config) - CLI: Values from command-line arguments
@click.command()
@AppArgs.generate_click_parameters()
def main(**kwargs: Any):
# Simple instantiation - no source tracking
config = AppArgs(**kwargs)
# Works fine, but config.source.* will always show CLITo enable accurate source tracking, use @click.pass_context and from_click_context():
@click.command()
@AppArgs.generate_click_parameters()
@click.pass_context
def main(ctx: click.Context, **kwargs: Any):
# Full source tracking with context
config = AppArgs.from_click_context(ctx, **kwargs)
# Check individual field sources
print(config.source.name) # ValueSource.CLI
print(config.source.age) # ValueSource.ENV
print(config.source.verbose) # ValueSource.DEFAULT
# Get summary of all sources
summary = config.get_sources_summary()
# {
# ValueSource.CLI: ['name'],
# ValueSource.ENV: ['age'],
# ValueSource.JSON: ['timeout'],
# ValueSource.DEFAULT: ['verbose']
# }See examples/source_tracking_comprehensive.py for a complete example showing all four sources working together. Run it with:
# With defaults only
python examples/source_tracking_comprehensive.py
# With environment variables
export MYAPP_TIMEOUT=120
export MYAPP_DEBUG=true
python examples/source_tracking_comprehensive.py
# Mix all sources (CLI > ENV > JSON > DEFAULT)
export MYAPP_TIMEOUT=120
python examples/source_tracking_comprehensive.py --config examples/sample_config.json --port 3000Output shows source for each field:
host = json-server.com [from JSON]
port = 3000 [from CLI] β CLI overrides JSON
debug = True [from ENV]
timeout = 120 [from ENV]
log_level = DEBUG [from JSON]
Values are resolved in the following order (highest to lowest priority):
- CLI arguments
- Environment variables
- Config file values
- Default values
wry automatically generates environment variable names from field names:
# Set environment variables
export WRY_NAME="Alice"
export WRY_AGE=25
# These will be picked up automatically
python myapp.py --verboseView supported environment variables:
python myapp.py --show-env-varsLoad configuration from JSON files:
python myapp.py --config settings.jsonWhere settings.json contains:
{
"name": "Bob",
"age": 35,
"verbose": true
}Use multiple Pydantic models in a single command:
from typing import Annotated
import click
from wry import WryModel, AutoOption, multi_model, create_models
class ServerConfig(WryModel):
host: Annotated[str, AutoOption] = "localhost"
port: Annotated[int, AutoOption] = 8080
class DatabaseArgs(WryModel):
db_url: Annotated[str, AutoOption] = "sqlite:///app.db"
pool_size: Annotated[int, AutoOption] = 5
@click.command()
@multi_model(ServerConfig, DatabaseConfig)
@click.pass_context
def serve(ctx: click.Context, **kwargs: Any):
# Create model instances
configs = create_models(ctx, kwargs, ServerConfig, DatabaseConfig)
server = configs[ServerConfig]
database = configs[DatabaseConfig]
print(f"Starting server at {server.host}:{server.port}")
print(f"Database: {database.db_url} (pool size: {database.pool_size})")Automatically generate options for all fields:
import click
from wry import AutoWryModel
from pydantic import Field
class QuickConfig(AutoWryModel):
"""All fields automatically become CLI options!"""
name: str = Field(description="Your name")
age: int = Field(default=30, ge=0, le=120)
email: str = Field(description="Your email")
@click.command()
@QuickConfig.generate_click_parameters()
def quickstart(config: QuickConfig):
print(f"Hello {config.name}!")Create configs without decorators:
from wry import WryModel
class Config(WryModel):
name: str = "default"
verbose: bool = False
# Create with source tracking
config = Config.create_with_sources(
name="Alice", # Will be tracked as programmatic source
verbose=True
)
# Or from Click context (in a command)
config = Config.from_click_context(ctx, **kwargs)Use multiple configuration models in a single command:
from wry import WryModel, multi_model, create_models
class DatabaseArgs(WryModel):
host: str = Field(default="localhost")
port: int = Field(default=5432)
class AppArgs(WryModel):
debug: bool = Field(default=False)
workers: int = Field(default=4)
@click.command()
@multi_model(DatabaseConfig, AppArgs)
@click.pass_context
def main(ctx: click.Context, **kwargs: Any):
# Automatically splits kwargs between models
configs = create_models(ctx, kwargs, DatabaseConfig, AppArgs)
db_config = configs[DatabaseConfig]
app_config = configs[AppArgs]
click.echo(f"Connecting to {db_config.host}:{db_config.port}")
click.echo(f"Running with {app_config.workers} workers")By default, generate_click_parameters runs in strict mode to prevent common mistakes:
@click.command()
@Config.generate_click_parameters() # strict=True by default
@Config.generate_click_parameters() # ERROR: Duplicate decorator detected!
def main(**kwargs: Any):
passTo allow multiple decorators (not recommended):
@Config.generate_click_parameters(strict=False)For more control over CLI generation, use the traditional WryModel with annotations:
from typing import Annotated
from wry import WryModel, AutoOption, AutoArgument
class Config(WryModel):
# Environment variable prefix
env_prefix = "MYAPP_"
# Required positional argument
input_file: Annotated[str, AutoArgument] = Field(
description="Input file path"
)
# Optional flag with short option
verbose: Annotated[bool, AutoOption] = Field(
default=False,
description="Enable verbose output"
)
# Option with validation
timeout: Annotated[int, AutoOption] = Field(
default=30,
ge=1,
le=300,
description="Timeout in seconds"
)New in v0.3.2+: Pydantic field aliases automatically control the generated CLI option names and environment variable names!
This allows you to have concise Python field names while exposing descriptive CLI options:
from pydantic import Field
from wry import AutoWryModel
class DatabaseConfig(AutoWryModel):
env_prefix = "DB_"
# Concise Python field name: db_url
# Alias controls CLI option: --database-url
# Environment variable: DB_DATABASE_URL
db_url: str = Field(
alias="database_url",
default="sqlite:///app.db",
description="Database connection URL"
)
pool_size: int = Field(
alias="connection_pool_size",
default=5,
description="Maximum connection pool size"
)How it works:
- Python field:
db_url(concise, easy to type) - CLI option:
--database-url(descriptive, user-friendly) - Environment variable:
DB_DATABASE_URL(consistent with CLI) - JSON config: Accepts both
db_urlanddatabase_url
Requirements:
- None!
WryModelautomatically setsvalidate_by_name=Trueandvalidate_by_alias=True- This tells Pydantic to accept both field names and aliases
- No need to configure anything - it just works!
- Aliases automatically control option names, env var names, and help text
Full support (v0.3.2+):
- β Aliases automatically control auto-generated option names
- β Environment variables use alias names (consistent with CLI)
- β Source tracking works correctly
- β JSON config accepts both field names and aliases
Why this feature exists:
Before v0.3.2, if you wanted custom CLI option names, you had to use explicit click.option() decorators for every field. The alias feature eliminates this boilerplate for the common case where you just want different names.
For advanced use cases (short options, custom Click types):
You can still combine aliases with explicit click.option() decorators:
class Config(AutoWryModel):
# Explicit click.option for short option support
verbose: Annotated[int, click.option("-v", "--verbose", count=True)] = Field(default=0)See examples/autowrymodel_comprehensive.py for examples of explicit Click decorators.
Automatic handling: wry automatically detects list[T] and tuple[T, ...] type fields and generates Click options with multiple=True.
from wry import AutoWryModel
class ProjectConfig(AutoWryModel):
tags: list[str] = Field(
default_factory=list,
description="Project tags"
)
ports: list[int] = Field(
default_factory=list,
description="Ports to expose"
)IMPORTANT - Usage: Users must pass the option multiple times (not comma-separated):
# β
CORRECT - Repeat the option for each value:
python app.py --tags python --tags rust --tags go
# Result: tags=["python", "rust", "go"]
# β
Also correct - Single value:
python app.py --tags python
# Result: tags=["python"]
# β
Also correct - No values (uses default):
python app.py
# Result: tags=[]
# β INCORRECT - Comma-separated does NOT work as expected:
python app.py --tags python,rust,go
# Result: tags=["python,rust,go"] β Single string with commas!Why not comma-separated?
This is Click's standard behavior with multiple=True. To parse comma-separated values, you would need a custom Click type. The current behavior matches standard Unix CLI conventions (similar to grep -e pattern1 -e pattern2 or docker run -v vol1 -v vol2).
Works with all list types:
tags: list[str] # String lists
ports: list[int] # Integer lists (with type validation)
flags: list[bool] # Boolean lists
items: tuple[str, ...] # Tuples also supportedJSON and Environment Variables:
{
"tags": ["python", "rust", "go"],
"ports": [8080, 8443]
}# JSON config works naturally with arrays
python app.py --config config.json
# CLI can override JSON values
python app.py --config config.json --tags typescript --tags javascriptTests: See tests/unit/auto_model/test_auto_model_list_fields.py for comprehensive examples.
If you prefer comma-separated input (like --tags python,rust,go) instead of repeating the option, wry provides two approaches:
Approach 1: Per-Field Annotation (fine-grained control)
from typing import Annotated
from wry import AutoWryModel, CommaSeparated
class Config(AutoWryModel):
# Standard behavior: --tags a --tags b --tags c
standard_tags: list[str] = Field(default_factory=list)
# Comma-separated: --csv-tags a,b,c
csv_tags: Annotated[list[str], CommaSeparated] = Field(
default_factory=list,
description="Tags (comma-separated)"
)Approach 2: Model-Wide Setting (all list fields)
from typing import ClassVar
from wry import AutoWryModel
class Config(AutoWryModel):
# Enable comma-separated for ALL list fields in this model
comma_separated_lists: ClassVar[bool] = True
# All these now accept comma-separated input
tags: list[str] = Field(default_factory=list) # --tags a,b,c
ports: list[int] = Field(default_factory=list) # --ports 80,443
values: list[float] = Field(default_factory=list) # --values 1.5,2.7Usage:
# Standard list field (multiple invocations)
python app.py --standard-tags a --standard-tags b --standard-tags c
# Comma-separated field (single invocation)
python app.py --csv-tags a,b,c
# Works with integers and floats too
python app.py --ports 8080,8443,9000
# With model-wide setting, all lists accept comma-separated
python app.py --tags a,b,c --ports 80,443 --values 1.5,2.7Which approach to use?
- Per-field: Use when only some lists should be comma-separated, or when mixing both styles
- Model-wide: Use when all lists in a model should consistently accept comma-separated input
Trade-offs:
- β
Pros: More concise, familiar to users of tools like
docker run -p 80,443 - β Pros: Model-wide setting is less verbose than annotating every field
β οΈ Cons: Cannot repeat the option (--csv-tags a,b --csv-tags c won't work)β οΈ Cons: Commas in values require escaping or quotingβ οΈ Cons: Less discoverable (users might not realize comma-separated is supported)
Recommendation: Use standard multiple=True (default) unless your users specifically expect comma-separated input. The model-wide comma_separated_lists setting is convenient when building tools with consistent comma-separated conventions (like Docker-style CLIs).
Implementation Notes:
comma_separated_listsis aClassVar[bool](likeenv_prefix)- NOT included in Pydantic fields (won't appear in
model_fieldsormodel_dump()) - Per-field
CommaSeparatedannotation takes priority over model-wide setting - Works with all list types:
list[str],list[int],list[float],tuple[T, ...] - Automatically strips whitespace and filters empty items
- Validates types and enforces Pydantic constraints
Tests: 22 comprehensive tests covering standard and comma-separated behavior
tests/unit/auto_model/test_auto_model_list_fields.py- Standard multiple=True (11 tests)tests/unit/auto_model/test_comma_separated_lists.py- Comma-separated support (11 tests)
See also:
examples/autowrymodel_comprehensive.py- Complete AutoWryModel example with aliasesexamples/wrymodel_comprehensive.py- WryModel with aliases and source tracking
New in v0.6.0+: Boolean fields automatically generate both on and off options using the --option/--no-option pattern, making CLI interfaces more explicit and user-friendly.
from wry import AutoWryModel
from pydantic import Field
class Config(AutoWryModel):
debug: bool = Field(default=False, description="Enable debug mode")
verbose: bool = Field(default=False, description="Verbose output")
# Generates CLI options:
# --debug/--no-debug
# --verbose/--no-verboseUsage:
# Explicitly enable
python app.py --debug --verbose
# Explicitly disable
python app.py --no-debug --no-verbose
# Mix both
python app.py --debug --no-verbosefrom typing import Annotated
from wry import AutoOption
class Config(AutoWryModel):
# Custom off-option name
verbose: Annotated[bool, AutoOption(flag_off_option="quiet")] = Field(
default=False, description="Verbose output"
)
# β --verbose/--quiet (instead of --verbose/--no-verbose)class Config(AutoWryModel):
# Per-field custom prefix (prefix appears before option name)
check: Annotated[bool, AutoOption(flag_off_prefix="skip")] = Field(
default=True, description="Run validation checks"
)
# β --check/--skip-check (instead of --check/--no-check)from typing import ClassVar
class Config(AutoWryModel):
# Set prefix for ALL boolean fields
wry_boolean_off_prefix: ClassVar[str] = "skip"
check: bool = Field(default=False) # --check/--skip-checkIf you prefer the old single-flag behavior for specific fields:
class Config(AutoWryModel):
# Most fields use on/off pattern
debug: bool = Field(default=False) # --debug/--no-debug
# Opt-out to single flag
simple: Annotated[bool, AutoOption(flag_enable_on_off=False)] = Field(
default=False, description="Simple mode"
)
# β --simple (single flag, old behavior)Boolean on/off works seamlessly with Pydantic aliases:
class Config(AutoWryModel):
dbg: bool = Field(alias="debug", default=False, description="Debug mode")
# β --debug/--no-debug (uses alias name)If the generated off-option would conflict with an existing field name, wry automatically detects this and falls back to a single flag with a warning:
class Config(AutoWryModel):
debug: bool = Field(default=False)
no_debug: str = Field(default="something") # Conflicts with --no-debug!
# Warning emitted: "Boolean field 'debug' off-option '--no-debug' collides with existing field 'no_debug'"
# Automatically falls back to single --debug flagTest Coverage:
tests/unit/auto_model/test_boolean_on_off_flags.py- Comprehensive tests (15 tests)tests/integration/test_boolean_flags_integration.py- Integration tests (8 tests)- All boolean features work with source tracking, JSON config, and environment variables
Design Rationale:
- Follows Click best practices: https://click.palletsprojects.com/en/stable/options/#boolean
- More explicit than single flags - users can clearly see both on and off options
- Backwards compatible via opt-out mechanism
- Follows PEP 593 Annotated pattern for customization
New in v0.3.3+: Both WryModel and AutoWryModel fully support inheritance! Create base configuration classes and extend them with additional fields.
from wry import AutoWryModel
from pydantic import Field
class BaseConfig(AutoWryModel):
"""Common configuration shared across environments."""
env_prefix = "APP_"
debug: bool = Field(default=False, description="Enable debug mode")
log_level: str = Field(default="INFO", description="Logging level")
class ProductionConfig(BaseConfig):
"""Production-specific configuration."""
workers: int = Field(default=4, ge=1, le=32, description="Number of workers")
timeout: int = Field(default=30, ge=1, description="Request timeout")
# ProductionConfig has all fields: debug, log_level, workers, timeout
@click.command()
@ProductionConfig.generate_click_parameters()
@click.pass_context
def serve(ctx: click.Context, **kwargs: Any):
config = ProductionConfig.from_click_context(ctx, **kwargs)
click.echo(f"Starting with {config.workers} workers, debug={config.debug}")class Level1Config(AutoWryModel):
field1: str = Field(default="v1", description="Level 1 field")
class Level2Config(Level1Config):
field2: str = Field(default="v2", description="Level 2 field")
class Level3Config(Level2Config):
field3: str = Field(default="v3", description="Level 3 field")
# Level3Config has all fields: field1, field2, field3from wry import multi_model, create_models
class BaseServer(AutoWryModel):
host: str = Field(default="localhost", description="Server host")
class ExtendedServer(BaseServer):
port: int = Field(default=8080, description="Server port")
class BaseDatabase(AutoWryModel):
db_url: str = Field(default="sqlite:///app.db", description="Database URL")
class ExtendedDatabase(BaseDatabase):
pool_size: int = Field(default=5, description="Connection pool size")
@click.command()
@multi_model(ExtendedServer, ExtendedDatabase)
@click.pass_context
def main(ctx: click.Context, **kwargs: Any):
configs = create_models(ctx, kwargs, ExtendedServer, ExtendedDatabase)
server = configs[ExtendedServer]
db = configs[ExtendedDatabase]
# Both base and extended fields are available
click.echo(f"Server: {server.host}:{server.port}")
click.echo(f"Database: {db.db_url} (pool={db.pool_size})")from typing import Annotated
from wry import WryModel, AutoWryModel, AutoOption
# Start with explicit WryModel
class BaseConfig(WryModel):
base_field: Annotated[str, AutoOption] = Field(default="base")
# Extend with AutoWryModel for automatic options
class ChildConfig(BaseConfig, AutoWryModel):
# This automatically becomes an option
child_field: str = Field(default="child")- DRY Principle: Define common fields once
- Environment-Specific Configs: Base config + dev/staging/prod extensions
- Feature Flags: Base config + feature-specific configurations
- Reusable Components: Create libraries of configuration classes
- Full Source Tracking: Inherited fields track their sources correctly
Important Notes:
- All inherited fields automatically become CLI options (for
AutoWryModel) - Source tracking works correctly for inherited fields
env_prefixcan be inherited or overridden- Field constraints and validation are inherited
- Works with all wry features: arguments, options, exclusions, aliases
See tests/features/test_inheritance.py for comprehensive examples.
- Python 3.10+
- Git with SSH key configured for signing
# Clone the repository
git clone git@github.com:tahouse/wry.git
cd wry
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install package in development mode with dev dependencies
pip install -e ".[dev]"
# Install pre-commit hooks
pre-commit install# Run all tests with coverage
pytest
# Run with coverage report
pytest --cov=wry --cov-report=html
# Run specific test
pytest tests/test_core.py::TestWryModel::test_basic_model_creation
# Run all checks (recommended before pushing)
./check.shwry supports Python 3.10, 3.11, and 3.12. To ensure compatibility:
# Test with all available Python versions locally
./scripts/test_all_versions.sh
# Test in CI-like environment using Docker
./scripts/test_ci_locally.sh
# Run GitHub Actions locally with act (requires act to be installed)
./scripts/test_with_act.shThis project uses pre-commit hooks to ensure code quality:
- ruff: Linting and code formatting
- mypy: Type checking (pinned to >=1.17.1)
- pytest: Tests with 90% coverage requirement
- bandit: Security checks
- safety: Dependency vulnerability scanning
Pre-commit will run automatically on git commit. To run manually:
pre-commit run --all-filesTo ensure consistent behavior between local development and CI:
- pydantic: >=2.9.2 (for proper type inference)
- mypy: >=1.17.1 (for accurate type checking)
- Python: 3.10+ (we test against 3.10, 3.11, and 3.12)
Install the exact versions used in CI:
pip install -e ".[dev]" --upgradeThis project enforces 90% code coverage. To check coverage locally:
pytest --cov=wry --cov-report=term-missing --cov-fail-under=90This project uses Git tags and GitHub Actions for releases. Only maintainers can create releases.
-
Ensure all changes are committed and pushed to
main -
Create and push a signed tag:
git tag -s v0.1.0 -m "Release version 0.1.0" git push origin v0.1.0 -
The CI/CD pipeline will automatically:
- Run all tests
- Build source and wheel distributions
- Upload to PyPI
- Create a GitHub release
- Sign artifacts with Sigstore
This project follows Semantic Versioning:
- MAJOR version for incompatible API changes
- MINOR version for new functionality (backwards compatible)
- PATCH version for backwards compatible bug fixes
Version numbers are managed by setuptools-scm and derived from git tags.
Every push to main creates a development release on PyPI:
pip install --pre wry # Install latest dev versionThe wry codebase is organized into focused modules:
Main Package:
wry/__init__.py: Package exports and version handlingwry/click_integration.py: Click-specific decorators and parameter generationwry/multi_model.py: Support for multiple models in single commandswry/auto_model.py: Zero-configuration model with automatic option generation
Core Subpackage (wry/core/):
model.py: CoreWryModelimplementation with value trackingsources.py: Value source definitions and trackingaccessors.py: Property accessors for field metadatafield_utils.py: Field constraint extraction and utilitiesenv_utils.py: Environment variable handling
- WRY (Why Repeat Yourself?): Define CLI structure once using Pydantic models
- Type Safety: Leverage Python's type system for validation and IDE support
- Explicit is Better: Users must opt-in to features like source tracking via
@click.pass_context - Composability: Mix and match models, decorators, and configurations
- Source Tracking: Always know where configuration values came from
We welcome contributions! Please see CONTRIBUTING.md for comprehensive guidelines.
Quick links:
- π
CONTRIBUTING.md- Complete contributor guide (for humans) - π€
.cursorrules- AI assistant quick reference - π
AI_KNOWLEDGE_BASE.md- Complete technical reference - π
RELEASE_PROCESS.md- Release workflow and versioning
We use standard Python virtual environments and pip for dependency management:
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install package in development mode with all dependencies
pip install -e ".[dev]"Note: All dependencies are specified in pyproject.toml. The [dev] extra includes testing, linting, and development tools.
# Install package in editable mode
pip install -e ".[dev]"
# Update dependencies
pip install -e ".[dev]" --upgrade
# Show installed packages
pip list
# Run tests
pytest
# Run with coverage
pytest --cov=wry
# Run linting
ruff check wry tests
ruff format wry tests
# Run type checking
mypy wry-
Fork the repository on GitHub
-
Clone your fork locally:
git clone git@github.com:YOUR_USERNAME/wry.git cd wry -
Add upstream remote:
git remote add upstream git@github.com:tahouse/wry.git
-
Create a feature branch:
git checkout -b feature/your-feature-name
-
Set up development environment:
python -m venv venv # Create virtual environment source venv/bin/activate # Activate it pip install -e ".[dev]" # Install with dev dependencies pre-commit install # Set up git hooks
-
Make your changes:
- Follow existing code style and patterns
- Add/update tests for new functionality
- Update documentation as needed
- Add docstrings to all new functions/classes
-
Test your changes:
# Run tests pytest # Check coverage (must be β₯90%) pytest --cov=wry --cov-report=term-missing # Run linting pre-commit run --all-files # Or run individual tools: ruff check wry tests mypy wry
-
Commit your changes:
- Use Conventional Commits format
- Examples:
feat: add support for YAML config filesfix: handle empty config files gracefullydocs: update examples for new APItest: add tests for edge casesrefactor: simplify value source tracking
-
Update your branch:
git fetch upstream git rebase upstream/main
-
Push to your fork:
git push origin feature/your-feature-name
-
Create Pull Request:
- Use a clear, descriptive title
- Reference any related issues
- Describe what changes you made and why
- Include examples if applicable
- Ensure all CI checks pass
- Use type hints for all function arguments and return values
- Follow PEP 8 (enforced by ruff)
- Maximum line length: 88 characters (Black's default)
- Use descriptive variable names
- Add docstrings to all public functions/classes/modules
- Write tests for all new functionality
- Maintain 100% code coverage
- Use pytest fixtures for common test setups
- Test both happy paths and edge cases
- Include tests for error conditions
- Update README.md if adding new features
- Add/update docstrings
- Include usage examples in docstrings
- Update type hints
- Bug fixes: Always welcome!
- New features: Please open an issue first to discuss
- Documentation: Improvements always appreciated
- Tests: Additional test cases for edge conditions
- Performance: Optimizations with benchmarks
- Examples: More usage examples
- Open an issue for bugs or feature requests
- Start a discussion for general questions
- Check existing issues/PRs before creating new ones
wry has comprehensive documentation for different audiences:
- π
README.md(this file) - Getting started, features, usage examples - π
examples/- Working code examplesexamples/autowrymodel_comprehensive.py- Complete AutoWryModel featuresexamples/wrymodel_comprehensive.py- WryModel with source trackingexamples/multimodel_comprehensive.py- Multi-model usage
- π
CONTRIBUTING.md- Complete contributor guide with code patterns and checklists - π€
.cursorrules- AI assistant quick reference (references CONTRIBUTING.md) - π
RELEASE_PROCESS.md- How to create releases and manage versions - π
TODO.md- Current tasks, planned features, and work in progress
- π
AI_KNOWLEDGE_BASE.md- Complete technical reference for AI/LLMs (also useful for humans) - π
CHANGELOG.md- Version history and all changes - π§ͺ
tests/README.md- Test organization and structure - π§
scripts/README.md- Development scripts and tools
I'm a user, I want to...
- Get started β README.md "Quick Start" section
- See examples β
examples/directory - Understand features β README.md "Features" section
- Track config sources β README.md "Value Source Tracking" section
I'm a contributor, I want to...
- Set up development β CONTRIBUTING.md "Development Setup" section
- Add a feature β CONTRIBUTING.md "Adding New Features" section
- Run tests β CONTRIBUTING.md "Testing" section
- Create a release β RELEASE_PROCESS.md
- Check current tasks β TODO.md
I'm an AI assistant, I want to...
- Quick reference β
.cursorrules - Technical details β
AI_KNOWLEDGE_BASE.md - Code patterns β
CONTRIBUTING.md - Test examples β
tests/features/test_source_precedence.py
This project is licensed under the MIT License - see the LICENSE file for details.
We're exploring a cleaner API that would automatically instantiate models and pass them to your function. See examples/auto_instantiate_poc.py for a working proof of concept:
# Potential future syntax
@click.command()
@AppConfig.click_command() # or @auto_instantiate(AppConfig)
def main(config: AppConfig):
"""The decorator would handle instantiation automatically."""
click.echo(f"Hello {config.name}!")
# Source tracking would work automatically too!This would:
- Automatically handle
@click.pass_contextwhen needed for source tracking - Instantiate the model and pass it with the correct parameter name
- Support multiple models in a single command
- Make the API more intuitive and similar to other libraries
If you're interested in this feature, please provide feedback!
- Built on top of Click and Pydantic
- Inspired by the DRY (Don't Repeat Yourself) principle
We'd also like to acknowledgme
pydanclick, which uses a similar clean syntax (no kwargs to command functions). The code for this feature will be independently written given thatwrysupports source tracking, constraint help text creation, instantiation from config files, and several other features not supported bypydanclick.