Pyrig’s multi-package architecture enables automatic discovery and inheritance of classes across your entire dependency chain. The .I and .L class properties provide a convenient way to access leaf implementations without manual imports or registration.
The .I pattern (“Instance”) and .L pattern (“Leaf”) are core to pyrig’s design. They enable configuration inheritance, tool customization, and plugin discovery across packages.
.I returns an instance of the leaf (most-derived) subclass:
from pyrig.rig.configs.pyproject import PyprojectConfigFile# Get instance of the leaf implementationinstance = PyprojectConfigFile.I# Access methods on the instancepath = PyprojectConfigFile.I.path()config = PyprojectConfigFile.I.load()PyprojectConfigFile.I.validate()
@classproperty@cachedef L(cls: type[Self]) -> type[Self]: """Get the final leaf subclass (deepest in the inheritance tree).""" has_leaf, subclasses = generator_has_items(cls.subclasses()) if not has_leaf: msg = f"No concrete subclasses found for {cls.__name__}" raise TypeError(msg) leaf = next(subclasses) second = next(subclasses, None) if second is not None: msg = ( f"Multiple concrete subclasses found for {cls.__name__}: " f"{', '.join(c.__name__ for c in (leaf, second, *subclasses))}" ) raise TypeError(msg) return leaf
The .L property:
Discovers all subclasses across the dependency chain
Filters to only concrete (non-abstract) subclasses
Discards parent classes (only keeps leaves)
Ensures exactly one leaf exists
Returns the leaf class
Caches the result
If multiple concrete subclasses exist, .L raises a TypeError. This ensures unambiguous inheritance chains.
The magic happens in discover_subclasses_across_dependents:
src/modules/package.py
def discover_subclasses_across_dependents( cls: type[T], dep: ModuleType, load_package_before: ModuleType,) -> Generator[type[T], None, None]: """Discover all subclasses of cls across packages depending on dep.""" # 1. Find all packages depending on dep (e.g., pyrig) all_packages = [dep, *all_deps_depending_on_dep(dep)] # 2. For each package, import the equivalent module for package in all_packages: package_module_name = load_package_before.__name__.replace( dep.__name__, package.__name__, 1 ) package_module = import_module(package_module_name) # 3. Discover subclasses in that module for subclass in discover_all_subclasses(cls): if subclass.__module__.startswith(package_module.__name__): yield subclass
from pyrig.rig.configs.pyproject import PyprojectConfigFile as Baseclass PyprojectConfigFile(Base): """Customized pyproject.toml for myapp.""" def _configs(self) -> ConfigDict: base = super()._configs() # Add custom configuration base["project"]["license"] = "MIT" base["tool"]["myapp"] = {"custom": "value"} return base
Now when pyrig discovers configs:
# From anywhere in the codebasefrom pyrig.rig.configs.pyproject import PyprojectConfigFile# Automatically uses YOUR customized versionconfig = PyprojectConfigFile.I.load() # Includes your customizations!
from pyrig.rig.tools.linter import Linterfrom pyrig.rig.tools.version_controller import VersionControllerfrom pyrig.rig.tools.package_manager import PackageManager# Run tools (uses your customizations if defined)Linter.I.check_args().run()VersionController.I.add_all_args().run()PackageManager.I.install_dependencies_args().run()# Get tool informationrepo_url = VersionController.I.repo_url()project_name = PackageManager.I.project_name()
from pyrig.rig.configs.base.base import ConfigFile# Validate all discovered config filesConfigFile.validate_all_subclasses()# This is what `pyrig mkroot` does!
Passing the class (not instance) to other functions
Type checking or introspection
# ✓ Use .L for manual instantiationLeafClass = PyprojectConfigFile.Linstance1 = LeafClass()instance2 = LeafClass()# ✓ Use .L for type checkingif isinstance(obj, PyprojectConfigFile.L): ...
No registration required. Just subclass and it’s automatically discovered.
# Just define the subclassclass MyPluginConfigFile(YamlConfigFile): ...# Automatically discovered and usedConfigFile.validate_all_subclasses() # Includes your plugin!
2. Dependency Injection Without Boilerplate
The .I pattern provides automatic dependency injection.
# No need to wire up dependenciesclass MyConfigFile(YamlConfigFile): def _configs(self) -> ConfigDict: # Just use .I - automatically gets the right implementation project = PyprojectConfigFile.I.load()["project"] return {"project_name": project["name"]}
3. Easy Customization
Override any class by subclassing with the same name.
# In your projectclass PyprojectConfigFile(Base): def _configs(self): # Add your customizations ...# Automatically used everywherePyprojectConfigFile.I.load() # Your version!
Single Leaf Requirement: Each base class can have only one concrete leaf subclass. Multiple concrete subclasses raise a TypeError.
# ✗ This will raise TypeErrorclass CustomConfig1(BaseConfig): ...class CustomConfig2(BaseConfig): ... # Error: multiple leaves!# ✓ Only one leaf per baseclass CustomConfig(BaseConfig): ...
Naming Convention: Keep the same class name when subclassing for automatic discovery. Use import aliases to avoid conflicts:
from pyrig.rig.configs.pyproject import PyprojectConfigFile as Baseclass PyprojectConfigFile(Base): # Same name = automatic discovery ...