Skip to main content

What are Tools?

Tools in pyrig are type-safe wrappers around command-line utilities used in Python development workflows. Each tool provides a clean, composable API for constructing commands without sacrificing the flexibility of raw CLI usage.

The Tool Pattern

All tools follow a consistent pattern:
  1. Each tool is a Tool subclass - Provides a consistent interface
  2. Tool methods return Args objects - Immutable command argument tuples
  3. Args objects execute directly - Call .run() to execute
  4. All command construction is centralized - Easy to test and maintain

Benefits

Type Safety

Arguments are validated at construction time, catching errors before execution

Composability

Args objects can be combined and extended for complex workflows

Testability

Command construction can be tested without executing subprocess calls

Discoverability

IDE autocomplete shows all available commands and their parameters

Quick Example

from pyrig.rig.tools.package_manager import PackageManager

# Construct command arguments
args = PackageManager.I.install_dependencies_args()

# Inspect the command
print(args)  # Output: uv sync

# Execute the command
result = args.run()
print(result.returncode)  # 0

Available Tools

Pyrig includes wrappers for common development tools:

Package Manager

UV package manager wrapper for dependency management

Version Controller

Git wrapper for version control operations

Linter

Ruff linter and formatter wrapper

Type Checker

Ty type checker wrapper

Project Tester

Pytest test runner wrapper

The Tool Base Class

Location

pyrig.rig.tools.base.base.Tool (rig/tools/base/base.py:49)

Core Methods

All Tool subclasses must implement:
from abc import abstractmethod
from pyrig.rig.tools.base.base import Tool
from pyrig.src.processes import Args

class MyTool(Tool):
    @abstractmethod
    def name(self) -> str:
        """Return the command name (e.g., 'git', 'uv', 'pytest')"""
        return "mytool"
    
    @abstractmethod
    def group(self) -> str:
        """Return the tool group for badge organization"""
        return ToolGroup.TOOLING
    
    @abstractmethod
    def badge_urls(self) -> tuple[str, str]:
        """Return (badge_image_url, project_url) for README badges"""
        return (
            "https://img.shields.io/badge/MyTool-blue",
            "https://example.com/mytool"
        )

The args() Method

The base args() method constructs command arguments:
def args(self, *args: str) -> Args:
    """Construct command arguments with tool name prepended.
    
    Args:
        *args: Command arguments.
    
    Returns:
        Args object with tool name and arguments.
    """
    return Args((self.name(), *args))

Example Usage

class MyTool(Tool):
    def name(self) -> str:
        return "mytool"
    
    def build_args(self, *args: str) -> Args:
        """Construct mytool build arguments."""
        return self.args("build", *args)
    
    def test_args(self, verbose: bool = False) -> Args:
        """Construct mytool test arguments."""
        base = self.args("test")
        if verbose:
            return Args((*base, "--verbose"))
        return base

# Usage
MyTool.I.build_args("--release").run()
MyTool.I.test_args(verbose=True).run()

The Args Class

Location

pyrig.src.processes.Args (src/processes.py:121)

Overview

Args is an immutable tuple subclass that represents a complete command ready for execution:
class Args(tuple[str, ...]):
    """Immutable command-line arguments container with execution capabilities."""
    
    def run(self, *args: str, **kwargs: Any) -> subprocess.CompletedProcess[Any]:
        """Execute command via subprocess."""
        return run_subprocess((*self, *args), **kwargs)
    
    def run_cached(self, *args: str, **kwargs: Any) -> subprocess.CompletedProcess[Any]:
        """Execute command via cached subprocess."""
        return run_subprocess_cached((*self, *args), **kwargs)

Execution Methods

Execute the command immediately:
args = PackageManager.I.add_dependencies_args("requests")
result = args.run()

# With additional options
result = args.run(check=False, cwd="/path/to/project")

Common Parameters

Both run() and run_cached() accept:
  • check: bool = True - Raise exception on non-zero exit code
  • capture_output: bool = True - Capture stdout/stderr
  • cwd: str | Path | None = None - Working directory
  • timeout: int | None = None - Command timeout in seconds
  • text: bool = True - Decode output as text

Customizing Tools

You can customize tools by subclassing them:
from pyrig.rig.tools.package_manager import PackageManager
from pyrig.src.processes import Args

class CustomPackageManager(PackageManager):
    """Package manager with custom defaults."""
    
    def install_dependencies_args(self, *args: str) -> Args:
        """Install dependencies with frozen versions."""
        return super().install_dependencies_args("--frozen", *args)
    
    def dev_dependencies(self) -> tuple[str, ...]:
        """Add custom dev dependencies."""
        return (*super().dev_dependencies(), "ipython", "ipdb")

Tool Groups

Tools are organized into groups for badge organization:
from pyrig.rig.tools.base.base import ToolGroup

ToolGroup.CI_CD          # ci/cd
ToolGroup.CODE_QUALITY   # code-quality
ToolGroup.DOCUMENTATION  # documentation
ToolGroup.PROJECT_INFO   # project-info
ToolGroup.SECURITY       # security
ToolGroup.TOOLING        # tooling
ToolGroup.TESTING        # testing

Best Practices

All tools provide a singleton instance via .I:
# Good
PackageManager.I.install_dependencies_args()

# Also works, but unnecessary
PackageManager().install_dependencies_args()
Build complex commands incrementally:
base = Linter.I.check_args()
with_fix = Args((*base, "--fix"))
with_specific_files = Args((*with_fix, "src/", "tests/"))
with_specific_files.run()
Test command construction without running subprocesses:
def test_install_command():
    args = PackageManager.I.install_dependencies_args("--no-dev")
    assert str(args) == "uv sync --no-dev"
    assert args == ("uv", "sync", "--no-dev")
Use check=False for commands that may fail:
result = VersionController.I.diff_quiet_args().run(check=False)
if result.returncode != 0:
    print("Uncommitted changes detected")

Next Steps

Package Manager

Learn about UV package management

Version Controller

Explore Git operations

Code Quality

Configure linting and formatting

Build docs developers (and LLMs) love