Extensions
RevitPy has an extension framework for building modular, reusable plugins. Extensions have a defined lifecycle, can declare commands, services, tools, and analyzers, and benefit from a built-in dependency injection container.
Extension Base Class
All extensions inherit from the abstract Extension class and must implement three lifecycle methods: load, activate, and deactivate.
from revitpy.extensions.extension import Extension, ExtensionMetadata
class MyExtension(Extension):
def __init__(self):
metadata = ExtensionMetadata(
name="My Extension",
version="1.0.0",
description="A sample extension",
author="Your Name",
)
super().__init__(metadata)
async def load(self) -> None:
"""Register services, set up configuration, prepare resources."""
self.log_info("Loading...")
async def activate(self) -> None:
"""Start background services, register commands and tools, show UI."""
self.log_info("Activating...")
async def deactivate(self) -> None:
"""Stop services, unregister commands, hide UI."""
self.log_info("Deactivating...")
Extension Lifecycle
Extensions go through these states, tracked by the ExtensionStatus enum:
| Status | Description |
|---|---|
UNLOADED |
Extension has been created but not loaded |
LOADING |
Load is in progress |
LOADED |
Extension has been loaded successfully |
INITIALIZING |
Activation is in progress |
ACTIVE |
Extension is fully active |
DEACTIVATING |
Deactivation is in progress |
DEACTIVATED |
Extension has been deactivated but can be reactivated |
ERROR |
An error occurred during a lifecycle transition |
DISPOSED |
Extension has been disposed and cannot be reactivated |
Lifecycle Methods
Use the public lifecycle management methods rather than calling load/activate/deactivate directly. These methods include error handling and callback execution:
ext = MyExtension()
# Load (sets up DI, loads config, calls load())
success = await ext.load_extension()
# Activate (calls activate(), discovers components)
success = await ext.activate_extension()
# Deactivate (unregisters components, calls deactivate())
success = await ext.deactivate_extension()
# Dispose (deactivates if needed, frees all resources)
await ext.dispose_extension()
Extension Properties
ext.name– Extension name from metadata.ext.version– Extension version from metadata.ext.extension_id– Unique UUID string.ext.status– CurrentExtensionStatus.ext.is_loaded–Trueif loaded (not unloaded, error, or disposed).ext.is_active–Trueif status isACTIVE.ext.has_error–Trueif status isERROR.ext.last_error– The last exception, orNone.
Lifecycle Callbacks
Register callbacks for lifecycle events:
ext.on_load(lambda e: print(f"{e.name} loaded"))
ext.on_activation(lambda e: print(f"{e.name} activated"))
ext.on_deactivation(lambda e: print(f"{e.name} deactivated"))
ext.on_disposal(lambda e: print(f"{e.name} disposed"))
Callbacks can be sync or async functions.
ExtensionMetadata
The ExtensionMetadata dataclass describes an extension:
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
Required | Extension name |
version |
str |
Required | Version string |
description |
str |
"" |
Description |
author |
str |
"" |
Author name |
website |
str |
"" |
Website URL |
license |
str |
"" |
License identifier |
dependencies |
list[str] |
[] |
Names of required extensions |
revit_versions |
list[str] |
[] |
Compatible Revit versions |
python_version |
str |
">=3.11" |
Required Python version |
provides_commands |
list[str] |
[] |
Declared command names |
provides_services |
list[str] |
[] |
Declared service names |
provides_tools |
list[str] |
[] |
Declared tool names |
provides_analyzers |
list[str] |
[] |
Declared analyzer names |
config_schema |
dict or None |
None |
Configuration schema |
default_config |
dict or None |
None |
Default configuration values |
ExtensionMetadata has a to_dict() method for serialization. The extension_id, load_time, and activation_time fields are set automatically.
ExtensionManager
ExtensionManager manages the lifecycle of all extensions. It is a singleton.
from revitpy import ExtensionManager
from revitpy.extensions.manager import ExtensionManagerConfig
config = ExtensionManagerConfig(
extension_directories=[Path("./extensions")],
auto_load_extensions=True,
auto_activate_extensions=True,
dependency_resolution=True,
max_load_retries=3,
extension_timeout=30.0,
)
manager = ExtensionManager(config)
# Initialize (registers core services, discovers extensions)
await manager.initialize()
# Or use as an async context manager
async with ExtensionManager(config) as manager:
# manager is initialized
pass
# manager.shutdown() is called automatically
Loading and Unloading
# Load by name or ID
success = await manager.load_extension("My Extension")
# Unload
success = await manager.unload_extension("My Extension")
# Activate / deactivate
success = await manager.activate_extension("My Extension")
success = await manager.deactivate_extension("My Extension")
Extension Discovery
Extensions are discovered in the directories listed in ExtensionManagerConfig.extension_directories:
discovered = await manager.discover_extensions()
Accessing Extensions
ext = manager.get_extension("My Extension")
all_exts = manager.get_extensions()
active = manager.get_active_extensions()
by_status = manager.get_extensions_by_status(ExtensionStatus.ACTIVE)
manager.has_extension("My Extension") # True/False
manager.is_extension_active("My Extension") # True/False
manager.extension_count # Total count
manager.active_extension_count # Active count
Statistics
stats = manager.get_statistics()
info = manager.get_extension_info("My Extension")
Component Decorators
RevitPy provides decorators to declare extension components. When an extension is activated, it auto-discovers methods decorated with these decorators.
@extension
Marks a class as an extension and attaches metadata:
from revitpy.extensions.decorators import extension
@extension(
name="My Extension",
version="1.0.0",
description="A sample extension",
author="Your Name",
dependencies=["Core Extension"],
)
class MyExtension(Extension):
async def load(self): ...
async def activate(self): ...
async def deactivate(self): ...
@command
Marks a method as a user-triggered command:
from revitpy.extensions.decorators import command
class MyExtension(Extension):
@command(
name="Rename Walls",
description="Rename all walls in the project",
icon="rename.png",
tooltip="Renames walls based on naming convention",
shortcut="Ctrl+Shift+R",
category="Editing",
enabled=True,
visible=True,
)
def rename_walls(self):
pass
# Access: ext.get_command("Rename Walls")
# All: ext.get_commands()
@service
Marks a method or class as a background service:
from revitpy.extensions.decorators import service
class MyExtension(Extension):
@service(
name="Model Watcher",
description="Watches for model changes",
auto_start=True,
singleton=True,
dependencies=["EventService"],
)
def model_watcher(self):
pass
# Access: ext.get_service("Model Watcher")
# All: ext.get_services()
@tool
Marks a method as an interactive tool:
from revitpy.extensions.decorators import tool
class MyExtension(Extension):
@tool(
name="Wall Placer",
description="Place walls interactively",
icon="wall.png",
category="Tools",
interactive=True,
preview=True,
)
def wall_placer(self):
pass
# Access: ext.get_tool("Wall Placer")
# All: ext.get_tools()
@analyzer
Marks a method as a model analyzer:
from revitpy.extensions.decorators import analyzer
class MyExtension(Extension):
@analyzer(
name="Clash Detector",
description="Detect clashes between elements",
element_types=["Wall", "Floor"],
categories=["Structural"],
real_time=True,
on_demand=True,
)
def clash_detector(self):
pass
# Access: ext.get_analyzer("Clash Detector")
# All: ext.get_analyzers()
Other Decorators
@panel(name, title, width, height, resizable, dockable, floating)– Marks a method as a UI panel.@startup(priority)– Marks a method as a startup task. Lower priority values run first.@shutdown(priority)– Marks a method as a shutdown task.@config(key, default_value, description, required, validator)– Marks a property as configurable.@permission(name, description, required, category)– Declares a permission requirement.@cache(ttl, max_size, key_func)– Caches method results with optional TTL and size limits.
Dependency Injection
RevitPy includes a dependency injection (DI) container in revitpy.extensions.dependency_injection.
DIContainer
The DIContainer class supports three service lifetimes:
| Lifetime | Description |
|---|---|
SINGLETON |
One instance for the entire application |
SCOPED |
One instance per scope |
TRANSIENT |
New instance every time |
Registering Services
from revitpy.extensions.dependency_injection import DIContainer
container = DIContainer()
# Register a singleton with an existing instance
container.register_singleton(MyService, instance=my_service_instance)
# Register a singleton by type (created on first resolve)
container.register_singleton(IMyService, implementation_type=MyService)
# Register with a factory function
container.register_singleton(IMyService, factory=lambda: MyService())
# Register scoped and transient services
container.register_scoped(IScopedService, implementation_type=ScopedService)
container.register_transient(ITransientService, implementation_type=TransientService)
Registration methods return the container for chaining:
container.register_singleton(ServiceA, instance=a).register_transient(ServiceB)
Resolving Services
service = container.get_service(IMyService)
The container automatically resolves constructor dependencies by inspecting type annotations. Circular dependencies are detected and raise a RuntimeError.
Scopes
with container.scope():
# Scoped services are created once within this block
service = container.get_service(IScopedService)
# Scoped services are disposed when the block exits
Or explicitly:
scope = container.create_scope()
with scope:
service = container.get_service(IScopedService)
Child Containers
Create a child container that inherits registrations from the parent:
child = container.create_child_container()
child.register_singleton(ISpecialService, instance=special)
# child can resolve services from both itself and the parent
DI Decorators
from revitpy.extensions.dependency_injection import singleton, transient, scoped, inject
@singleton
class MyService:
pass
@singleton(IMyService) # Register as an interface
class MyServiceImpl:
pass
@transient
class PerRequestService:
pass
@scoped
class PerScopeService:
pass
@inject
def my_function(service: IMyService):
# service is automatically resolved from the global container
service.do_work()
Global Container
from revitpy.extensions.dependency_injection import (
set_current_container,
get_current_container,
get_service,
)
set_current_container(container)
# Resolve from the global container
service = get_service(IMyService)