Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Winipedia/pyrig/llms.txt
Use this file to discover all available pages before exploring further.
Overview
pyrig’s most powerful feature is its multi-package inheritance architecture. You can create a personal pyrig package with your own standards, add it as a dependency to any project, and have pyrig init automatically apply everything — configs, tools, CLI commands, and builders.
This guide shows you how to:
- Create a personal pyrig package
- Override default behaviors using the
.I pattern
- Understand dependency chain discovery
- Package and distribute your extensions
Understanding the .I Pattern
The .I pattern is pyrig’s mechanism for automatic subclass discovery and instantiation. It stands for Instance and provides access to the final (leaf) implementation in the dependency chain.
How It Works
Every extensible class in pyrig inherits from DependencySubclass, which provides:
from pyrig.src.subclass import DependencySubclass
class Tool(DependencySubclass):
@classmethod
def definition_package(cls) -> ModuleType:
"""Package where subclasses are defined."""
return tools
@classmethod
def sorting_key(cls, subclass: type[Self]) -> Any:
"""Sort key for ordering subclasses."""
return subclass().name()
When you call ToolName.I, pyrig:
- Discovers all packages that depend on pyrig
- Searches for
ToolName subclasses in <package>.rig.tools
- Validates that exactly one leaf subclass exists
- Returns an instance of that subclass
Example: Using .I
from pyrig.rig.tools.linter import Linter
from pyrig.rig.tools.package_manager import PackageManager
# Get the final implementation (could be yours!)
linter = Linter.I
package_manager = PackageManager.I
# Use it
linter.check_args().run()
package_manager.install_dependencies_args().run()
Creating a Personal pyrig Package
A personal pyrig package lets you standardize your workflow across all projects.
Step 1: Initialize Your Package
mkdir my-pyrig
cd my-pyrig
uv init
uv add pyrig
Step 2: Create the Extension Structure
Create the required directory structure:
mkdir -p my_pyrig/rig/tools
mkdir -p my_pyrig/rig/configs
mkdir -p my_pyrig/rig/cli/subcommands
touch my_pyrig/__init__.py
touch my_pyrig/rig/__init__.py
touch my_pyrig/rig/tools/__init__.py
touch my_pyrig/rig/configs/__init__.py
touch my_pyrig/rig/cli/__init__.py
touch my_pyrig/rig/cli/subcommands/__init__.py
The structure should mirror pyrig’s:
my_pyrig/
├── rig/
│ ├── tools/ # Tool overrides
│ ├── configs/ # Config overrides
│ ├── cli/
│ │ └── subcommands/ # Custom CLI commands
│ └── builders/ # Custom builders (optional)
└── src/ # Shared utilities (optional)
Create my_pyrig/rig/tools/linter.py:
from pyrig.rig.tools.linter import Linter as PyrigLinter
from pyrig.src.processes import Args
class Linter(PyrigLinter):
"""Custom linter with organization-specific settings."""
def check_args(self, *args: str) -> Args:
"""Always check with strict mode."""
return super().check_args("--select", "ALL", *args)
def format_args(self, *args: str) -> Args:
"""Format with 100-character line length."""
return self.args("format", "--line-length", "100", *args)
Now when any project uses Linter.I with your package installed, it will use your implementation!
Step 4: Add a Custom Config
Create my_pyrig/rig/configs/company_config.py:
from pathlib import Path
from pyrig.rig.configs.base.toml import TomlConfigFile
from pyrig.rig.configs.base.base import Priority
class CompanyConfig(TomlConfigFile):
"""Company-specific configuration."""
def parent_path(self) -> Path:
"""Place in project root."""
return Path()
def _configs(self) -> dict:
"""Required configuration."""
return {
"company": {
"name": "Acme Corp",
"team": "Platform",
"oncall": "platform-oncall@acme.com"
}
}
def priority(self) -> float:
"""Validate after pyproject.toml."""
return Priority.MEDIUM
When pyrig init or pyrig mkroot runs, this config will be automatically discovered and validated!
Step 5: Add a Custom CLI Command
Create my_pyrig/rig/cli/subcommands/deploy.py:
import typer
from pyrig.rig.tools.package_manager import PackageManager
def deploy(
environment: str = typer.Argument(..., help="Deployment environment"),
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deployed"),
) -> None:
"""Deploy the project to the specified environment."""
typer.echo(f"Deploying to {environment}...")
if dry_run:
typer.echo("Dry run - no actual deployment")
return
# Build and publish
PackageManager.I.build_args().run()
typer.echo(f"Deployed to {environment}!")
This command is automatically discovered and added to all projects that depend on your package:
uv run my-project deploy production
Dependency Chain Discovery
pyrig uses DependencyGraph to discover all packages in the dependency chain. Here’s how it works:
Discovery Algorithm
- Build Dependency Graph: Parse all installed packages and their dependencies
- Find Dependents: Identify all packages that depend on
pyrig
- Topological Sort: Order packages from base (pyrig) to leaves (your project)
- Discover Subclasses: For each package, search
<package>.rig.<category> for subclasses
Example Chain
pyrig (base)
↓
my-pyrig (your standards)
↓
my-project (specific overrides)
When my-project calls Linter.I:
- Searches
pyrig.rig.tools.linter → finds base Linter
- Searches
my_pyrig.rig.tools.linter → finds your Linter (overrides base)
- Searches
my_project.rig.tools.linter → if exists, overrides yours
- Returns the final (leaf) implementation
The .L Property
The .L property (Leaf) returns the class, not an instance:
# Get the class
LinterClass = Linter.L # type: type[Linter]
# Get an instance
linter = Linter.I # type: Linter
# Equivalent to:
linter = Linter.L()
Overriding Default Behaviors
Extend or modify any tool’s behavior:
from pyrig.rig.tools.project_tester import ProjectTester as PyrigTester
from pyrig.src.processes import Args
class ProjectTester(PyrigTester):
"""Custom test runner with coverage enforcement."""
def test_args(self, *args: str) -> Args:
"""Run tests with strict coverage."""
return super().test_args(
"--cov-fail-under=95",
"--cov-report=html",
*args
)
Override Config Defaults
Change default configuration values:
from pyrig.rig.configs.pyproject import PyprojectConfigFile
class PyprojectConfigFile(PyprojectConfigFile):
"""Custom pyproject.toml with organization defaults."""
def _configs(self) -> dict:
"""Merge org defaults with pyrig defaults."""
config = super()._configs()
# Add custom metadata
config["project"]["authors"] = [{
"name": "Acme Platform Team",
"email": "platform@acme.com"
}]
# Customize ruff settings
config["tool"]["ruff"]["line-length"] = 100
return config
Override Badge URLs
Customize README badges:
from pyrig.rig.tools.linter import Linter as PyrigLinter
class Linter(PyrigLinter):
def badge_urls(self) -> tuple[str, str]:
"""Link to internal ruff documentation."""
badge, _ = super().badge_urls()
return (badge, "https://internal.acme.com/docs/ruff")
Override Dev Dependencies
Change which dependencies are added for a tool:
from pyrig.rig.tools.linter import Linter as PyrigLinter
class Linter(PyrigLinter):
def dev_dependencies(self) -> tuple[str, ...]:
"""Use specific ruff version."""
return ("ruff>=0.15.0,<0.16.0",)
Package Structure Best Practices
Minimal Package
For simple overrides:
my_pyrig/
├── rig/
│ ├── tools/
│ │ ├── __init__.py
│ │ └── linter.py # Override one tool
│ └── __init__.py
├── __init__.py
└── pyproject.toml
Full-Featured Package
For comprehensive standards:
my_pyrig/
├── rig/
│ ├── tools/ # Tool overrides
│ │ ├── __init__.py
│ │ ├── linter.py
│ │ └── package_manager.py
│ ├── configs/ # Config overrides
│ │ ├── __init__.py
│ │ ├── pyproject.py
│ │ └── company_config.py
│ ├── cli/
│ │ ├── subcommands/ # Custom commands
│ │ │ ├── __init__.py
│ │ │ ├── deploy.py
│ │ │ └── validate.py
│ │ └── __init__.py
│ ├── builders/ # Custom builders
│ │ ├── __init__.py
│ │ └── docker_builder.py
│ └── __init__.py
├── src/ # Shared utilities
│ ├── __init__.py
│ └── company_utils.py
├── tests/ # Tests for your package
│ └── __init__.py
├── __init__.py
└── pyproject.toml
Testing Your Extensions
Test that your overrides work correctly:
from my_pyrig.rig.tools.linter import Linter
def test_linter_override():
"""Verify our Linter is discovered."""
# The .I pattern should find our implementation
from pyrig.rig.tools.linter import Linter as BaseLinter
assert BaseLinter.I.__class__.__module__ == "my_pyrig.rig.tools.linter"
assert isinstance(BaseLinter.I, Linter)
def test_custom_config_discovered():
"""Verify custom configs are discovered."""
from pyrig.rig.configs.base.base import ConfigFile
from my_pyrig.rig.configs.company_config import CompanyConfig
subclasses = list(ConfigFile.subclasses())
assert any(cls.__name__ == "CompanyConfig" for cls in subclasses)
Using Your Package
In New Projects
# Create new project
uv init my-new-project
cd my-new-project
# Add your package
uv add pyrig my-pyrig
# Initialize - uses YOUR standards!
uv run pyrig init
In Existing Projects
# Add to existing project
uv add my-pyrig
# Re-run init to apply your standards
uv run pyrig init
All your overrides, configs, and commands are automatically applied!
Advanced Patterns
Conditional Overrides
Apply different behaviors based on context:
import os
from pyrig.rig.tools.package_manager import PackageManager as PyrigPM
from pyrig.src.processes import Args
class PackageManager(PyrigPM):
def publish_args(self, *args: str, token: str) -> Args:
"""Auto-detect registry from environment."""
registry = os.getenv("PYPI_REGISTRY", "https://pypi.org")
return self.args(
"publish",
"--registry", registry,
"--token", token,
*args
)
Shared Utilities
Create utilities for all your projects:
# my_pyrig/src/company_utils.py
def get_team_oncall(team: str) -> str:
"""Get oncall email for a team."""
return f"{team}-oncall@acme.com"
# my_pyrig/rig/cli/subcommands/oncall.py
import typer
from my_pyrig.src.company_utils import get_team_oncall
def oncall(team: str) -> None:
"""Show oncall for a team."""
typer.echo(f"Oncall: {get_team_oncall(team)}")
Multi-Layer Inheritance
Create organization, team, and project layers:
pyrig (base framework)
↓
acme-pyrig (company standards)
↓
acme-platform-pyrig (team standards)
↓
my-service (project-specific)
Each layer can override the previous one!
Next Steps
Common Patterns
Organization-Wide Standards
# acme_pyrig/rig/configs/pyproject.py
class PyprojectConfigFile(PyprojectConfigFile):
def _configs(self) -> dict:
config = super()._configs()
config["project"]["authors"] = [{"name": "Acme Corp"}]
config["tool"]["ruff"]["line-length"] = 100
return config
Team-Specific Workflows
# platform_pyrig/rig/cli/subcommands/deploy.py
def deploy(env: str) -> None:
"""Deploy using platform's pipeline."""
# Team-specific deployment logic
pass
Project-Specific Overrides
# my_service/rig/tools/linter.py
class Linter(Linter):
def check_args(self, *args: str) -> Args:
# Project-specific lint exceptions
return super().check_args("--ignore", "D100", *args)