Mode Handlers¶
A ModeHandler lets you split provider behaviour by workspace role — root,
member, or standalone — without crowding your Provider subclass with
if mode == ... branches.
Workspace modes recap¶
Every repolish session runs in one of three modes:
| Mode | When it applies |
|---|---|
standalone |
A plain, single-project repository |
root |
The top-level repo in a uv workspace (monorepo) |
member |
A package inside a monorepo |
The mode is available in any provider hook via
context.repolish.workspace.mode.
Attaching handlers to a provider¶
Declare handler classes on the provider using the root_mode, member_mode,
and standalone_mode class attributes. Any attribute that is not set falls back
to the provider's own implementation (or the base-class no-op if neither defines
it).
from repolish import BaseContext, BaseInputs, ModeHandler, Provider, Symlink
class WorkspaceCtx(BaseContext):
tool_version: str = 'latest'
class RootHandler(ModeHandler[WorkspaceCtx, BaseInputs]):
def create_file_mappings(self, context):
# the root gets the aggregating task-runner config
return {'poe_tasks.toml': '_repolish.poe_tasks.root.toml'}
def create_default_symlinks(self):
# only the root should expose this symlink
return [Symlink(source='configs/root-config.yaml', target='.config/root.yaml')]
class MemberHandler(ModeHandler[WorkspaceCtx, BaseInputs]):
def create_file_mappings(self, context):
# members get a simpler per-package config
return {'poe_tasks.toml': '_repolish.poe_tasks.member.toml'}
# create_default_symlinks not overridden → no symlinks for members
class WorkspaceProvider(Provider[WorkspaceCtx, BaseInputs]):
root_mode = RootHandler
member_mode = MemberHandler
# standalone_mode not set → falls back to the Provider base no-ops
Resolution order¶
When repolish calls any hook it uses call_provider_method internally:
- Read
context.repolish.workspace.mode. - Look up the matching handler class (
root_mode,member_mode, orstandalone_mode). - If a handler class is registered, instantiate it (once, then cache it) and call the hook on it.
- If no handler is registered — or if the provider overrides the hook directly — call the provider's own implementation.
Direct overrides on the Provider subclass always take priority over mode
handlers. This means you can keep shared logic on the provider and add
mode-specific overrides in handlers.
Handler attributes¶
When a handler is first created repolish copies the provider's identity attributes onto it so handlers can reference them without extra plumbing:
| Attribute | Value |
|---|---|
alias |
Config key assigned by the loader |
version |
Package version (auto-detected) |
package_name |
Top-level import name |
project_name |
Distribution name from pyproject.toml |
templates_root |
provider_root/{mode}/ — mode-scoped template directory |
templates_root is the only attribute that is mode-specific. For root
mode it resolves to provider_root/root/, for member to
provider_root/member/, etc. Use it to glob mode-specific templates:
class RootHandler(ModeHandler[WorkspaceCtx, BaseInputs]):
def create_file_mappings(self, context):
# discover every workflow template specific to root
workflows = list(self.templates_root.glob('.github/workflows/*.yaml'))
return {f.name: str(f) for f in workflows}
Symlinks per mode¶
create_default_symlinks takes no context argument. This is intentional:
repolish link also calls this method when registering the provider, and at
that point no typed context has been built yet. Keep the return values static —
use self.templates_root for path construction if needed, but do not rely on
any context fields.
Use separate mode handlers to return different symlinks per workspace role — a
root_mode handler returns the symlinks and the member_mode handler simply
returns []:
class RootHandler(ModeHandler[WorkspaceCtx, BaseInputs]):
def create_default_symlinks(self):
return [Symlink(source='configs/shared.yaml', target='.shared.yaml')]
class MemberHandler(ModeHandler[WorkspaceCtx, BaseInputs]):
def create_default_symlinks(self):
return [] # no symlinks for members
Symlinks are created by both repolish link and repolish apply, so they are
always in place regardless of which command runs first.
Supported hooks¶
Every Provider hook is available on ModeHandler. Override only the ones that
differ across modes:
| Hook | Purpose |
|---|---|
provide_inputs |
Emit data to other providers |
finalize_context |
Merge received inputs into context |
create_file_mappings |
Return template→destination mappings |
promote_file_mappings |
Promote files to the monorepo root (member mode only) |
create_anchors |
Return anchor substitutions |
create_default_symlinks |
Return symlinks to create |