Skip to content

userconfig

Handle reading and parsing, writing and saving of all user configurations.

This module handles the reading and writing of the pyRevit configuration files. It's been used extensively by pyRevit sub-modules. user_config is set up automatically in the global scope by this module and can be imported into scripts and other modules to access the default configurations.

All other modules use this module to query user config.

Examples:

from pyrevit.userconfig import user_config
user_config.add_section('newsection')
user_config.newsection.property = value
user_config.newsection.get_option('property', default_value)
user_config.save_changes()

The user_config object is also the destination for reading and writing configuration by pyRevit scripts through pyrevit.script.get_config.

Examples:

from pyrevit import script
cfg = script.get_config()
cfg.property = value
cfg.get_option('property', default_value)
script.save_config()

Typed section access is the canonical way to reach the built-in settings. The core, routes, and telemetry properties expose the strongly-typed sections backed by the shared configuration service, so a setting is read and written by its section and its C# property name:

Examples:

user_config.core.RocketMode = True
value = user_config.core.RocketMode

The flat snake_case accessors (e.g. user_config.rocket_mode) are aliases over these sections. They are frozen, not deprecated: they keep working for reads and writes indefinitely, none is scheduled for removal, and no new one may be added. That is a decision, not an oversight - see docs/config-api.md for the naming contract, the full alias inventory, and the reasoning.

Important: a typed section falls back to a raw option for any name the C# schema does not declare, on read and on write alike. So user_config.core.rocket_mode = True stores a raw key named rocket_mode in a section whose real key is rocketmode; it does not raise, and it leaves RocketMode untouched. Use the PascalCase property name, or set_option for a key the schema has no property for.

Examples:

user_config.core.set_option("my_key", value)   # explicit: raw option
user_config.core.my_key                        # reads it back

add_section/get_section and the per-section get_option/set_option are the escape hatch for anything outside the typed schema, and pyrevit.coreutils.configparser.open_config_file covers a tool's own ini.

Attributes

DEFAULT_CSV_SEPARATOR = ',' module-attribute

mlogger = logger.get_logger(__name__) module-attribute

CONSTS = PyRevit.PyRevitConsts module-attribute

Re-exported for extensions that do from pyrevit.userconfig import CONSTS.

user_config = _build_user_config() module-attribute

Classes

PyRevitConfig(config_service)

Bases: object

Provide read/write access to pyRevit configuration.

Parameters:

Name Type Description Default
config_service IConfigurationService

configuration service to start from. Access resolves against the shared service the store hands out, falling back to this one while the store is unreachable.

required

Examples:

cfg = PyRevitConfig(config_service)
cfg.add_section('sectionname')
cfg.sectionname.property = value
cfg.sectionname.get_option('property', default_value)
cfg.save_changes()

Store an accessor for config_service, not the service itself.

Sections handed to scripts hold a bound method, so they follow a later reload instead of continuing to read a replaced service whose edits are never flushed.

Source code in pyrevitlib/pyrevit/userconfig.py
def __init__(self, config_service):
    """Store an accessor for config_service, not the service itself.

    Sections handed to scripts hold a bound method, so they follow a
    later reload instead of continuing to read a replaced service whose
    edits are never flushed.
    """
    self._config_service = config_service
    self.config_sections = ConfigSections(self._current_config_service)

    self._update_env()

Attributes

config_sections = ConfigSections(self._current_config_service) instance-attribute
config_service property

IConfigurationService: shared service backing this configuration.

config_file property

Path of the config file backing this configuration.

This is the file the service resolved on load, which may be a read-only all-users config.

environment property

Environment section.

core property

Core section (supports .get_option/.set_option for extension compat).

routes property

Routes section (supports .get_option/.set_option for extension compat).

telemetry property

Telemetry section (supports .get_option/.set_option for extension compat).

bin_cache property writable

Whether to use the cache for extensions.

check_updates property writable

Whether to check for updates.

auto_update property writable

Whether to automatically update pyRevit.

rocket_mode property writable

Whether to enable rocket mode.

log_level property writable

Logging level.

file_logging property writable

Whether to enable file logging.

startuplog_timeout property writable

Timeout for the startup log.

required_host_build property writable

Host build required to run the commands.

min_host_drivefreespace property writable

Minimum free space for running the commands.

load_beta property writable

Whether to load commands in beta.

read_script_metadata property writable

Whether to read script metadata (title, author, etc) from Python scripts.

When False, pyRevit will skip reading metadata from .py script files and will only use values from bundle.yaml. This improves startup performance but may affect commands that rely on script-level metadata.

output_close_others property writable

Whether to close other output windows.

output_close_mode_enum property writable

Output window closing mode as enum (CurrentCommand | CloseAll).

cpython_engine_version property writable

CPython engine version to use.

user_locale property writable

User locale.

output_stylesheet property writable

Stylesheet used for output.

routes_host property writable

Routes API host.

routes_port property writable

API routes port.

load_core_api property writable

Whether to load pyRevit core api.

telemetry_utc_timestamp property writable

Whether to use UTC timestamps in telemetry.

telemetry_status property writable

Telemetry status.

telemetry_file_dir property writable

Telemetry file directory.

telemetry_server_url property writable

Telemetry server URL.

telemetry_include_hooks property writable

Whether to include hooks in telemetry.

apptelemetry_status property writable

App telemetry status.

apptelemetry_server_url property writable

App telemetry server URL.

apptelemetry_event_flags property writable

App telemetry event flags.

user_can_update property writable

Whether the user can update pyRevit repos.

user_can_extend property writable

Whether the user can manage pyRevit Extensions.

user_can_config property writable

Whether the user can access the configuration.

colorize_docs property writable

Whether to enable the document colorizer.

tooltip_debug_info property writable

Whether to append debug info on tooltips.

routes_server property writable

Whether the server routes are enabled.

agent_enabled property writable

Whether the agent host (MCP) starts on the next pyRevit load.

The agent host is independent of the routes server: it listens on a current-user named pipe, never on the network.

Note

The setters write the config file immediately, through the same accessors the agent host and the pyrevit CLI use.

agent_policy property writable

What agent runs may do.

readonly allows queries and dry runs only, ask needs approval in Revit for every change, and auto commits changes without the prompt.

agent_engine property writable

Script engine for agent runs that don't name one: ironpython or cpython.

agent_user_skills_enabled property writable

Whether the MCP server loads user-authored skills for this user.

User skills are trusted-account guidance. They cannot replace shipped skills or relax host policy.

is_readonly property

bool: whether the config is read only.

config_type property

str: "Admin" for a read-only admin config, "User" otherwise.

Methods:

get_thirdparty_ext_root_dirs(include_default=True)

Return a list of external extension directories set by the user.

Ordering is deterministic: the default path first, then config-file order, with duplicates removed. User paths are resolved through the shared C# core so the loader and CLI agree on normalization and existence.

Returns:

Type Description
list[str]

External user extension directories.

Source code in pyrevitlib/pyrevit/userconfig.py
def get_thirdparty_ext_root_dirs(self, include_default=True):
    """Return a list of external extension directories set by the user.

    Ordering is deterministic: the default path first, then config-file
    order, with duplicates removed. User paths are resolved through the
    shared C# core so the loader and CLI agree on normalization and
    existence.

    Returns:
        (list[str]): External user extension directories.
    """
    seen = set()
    dir_list = []
    if include_default:
        norm = op.normpath(THIRDPARTY_EXTENSIONS_DEFAULT_DIR)
        if norm not in seen:
            seen.add(norm)
            dir_list.append(norm)
    try:
        for x in PyRevit.PyRevitExtensions.ResolveUserExtensionPaths():
            if x not in seen:
                seen.add(x)
                dir_list.append(x)
    except Exception as read_err:
        mlogger.error(
            "Error reading list of user extension folders. | %s", read_err
        )

    return [x for x in dir_list if op.exists(x)]
get_ext_root_dirs()

Return a list of all extension directories.

Returns:

Type Description
list[str]

user extension directories.

Source code in pyrevitlib/pyrevit/userconfig.py
def get_ext_root_dirs(self):
    """Return a list of all extension directories.

    Returns:
        (list[str]): user extension directories.

    """
    dir_list = set()
    if op.exists(EXTENSIONS_DEFAULT_DIR):
        dir_list.add(EXTENSIONS_DEFAULT_DIR)
    dir_list.update(self.get_thirdparty_ext_root_dirs())
    return list(dir_list)
get_ext_sources()

Return a list of extension definition source files.

Source code in pyrevitlib/pyrevit/userconfig.py
def get_ext_sources(self):
    """Return a list of extension definition source files."""
    return list(set(self.environment.Sources or []))
set_thirdparty_ext_root_dirs(path_list)

Updates list of external extension directories in config file.

Parameters:

Name Type Description Default
path_list list[str]

list of external extension paths

required
Source code in pyrevitlib/pyrevit/userconfig.py
def set_thirdparty_ext_root_dirs(self, path_list):
    """Updates list of external extension directories in config file.

    Args:
        path_list (list[str]): list of external extension paths
    """
    for ext_path in path_list:
        if not op.exists(ext_path):
            raise PyRevitException('Path "%s" does not exist.' % ext_path)

    try:
        self.core.UserExtensions = framework.to_clr_list(
            str, [op.normpath(x) for x in path_list]
        )
    except Exception as write_err:
        mlogger.error(
            "Error setting list of user extension folders. | %s", write_err
        )
get_current_attachment(cached=True)

Return current pyRevit attachment.

The shared C# attachment cache lets all script engines reuse a single disk lookup. Cleared on reload and on Attach/Detach/clone changes.

Parameters:

Name Type Description Default
cached bool

set False to bypass the session cache when the attachment may have changed out of process.

True
Source code in pyrevitlib/pyrevit/userconfig.py
def get_current_attachment(self, cached=True):
    """Return current pyRevit attachment.

    The shared C# attachment cache lets all script engines reuse a single
    disk lookup. Cleared on reload and on Attach/Detach/clone changes.

    Args:
        cached (bool): set False to bypass the session cache when the
            attachment may have changed out of process.
    """
    try:
        host_version = int(HOST_APP.version)
        if cached:
            return PyRevit.PyRevitAttachments.GetAttachedCached(host_version)
        return PyRevit.PyRevitAttachments.GetAttached(host_version)
    except PyRevitException as ex:
        mlogger.error("Error getting current attachment. | %s", ex)
get_active_cpython_engine()

Return the user's configured CPython engine, or the latest available.

Falls back to a temp clone at HOME_DIR when there's no attachment, and to the highest-versioned engine when the configured version isn't among the clone's available engines.

Source code in pyrevitlib/pyrevit/userconfig.py
def get_active_cpython_engine(self):
    """Return the user's configured CPython engine, or the latest available.

    Falls back to a temp clone at HOME_DIR when there's no attachment,
    and to the highest-versioned engine when the configured version isn't
    among the clone's available engines.
    """
    attachment = self.get_current_attachment()
    if attachment and attachment.Clone:
        clone = attachment.Clone
    else:
        try:
            clone = PyRevit.PyRevitClone(clonePath=HOME_DIR)
        except Exception as cEx:
            mlogger.debug("Can not create clone from path: %s", str(cEx))
            clone = None
    engines = clone.GetCPythonEngines() if clone else []
    cpy_engines_dict = {x.Version: x for x in engines}
    mlogger.debug("cpython engines dict: %s", cpy_engines_dict)

    if not cpy_engines_dict:
        mlogger.error(
            "Can not determine cpython engines for current attachment: %s",
            attachment,
        )
        return None
    try:
        cpyengine_ver = int(self.cpython_engine_version)
    except (ValueError, TypeError):
        cpyengine_ver = 000

    try:
        return cpy_engines_dict[cpyengine_ver]
    except KeyError:
        return max(cpy_engines_dict.values(), key=lambda x: x.Version.Version)
set_active_cpython_engine(pyrevit_engine)

Set the active CPython engine.

Parameters:

Name Type Description Default
pyrevit_engine PyRevitEngine

python engine to set as active

required
Source code in pyrevitlib/pyrevit/userconfig.py
def set_active_cpython_engine(self, pyrevit_engine):
    """Set the active CPython engine.

    Args:
        pyrevit_engine (PyRevitEngine): python engine to set as active
    """
    self.cpython_engine_version = pyrevit_engine.Version
save_changes()

Flush the in-memory config to disk.

Typed and dynamic section edits are already written through to the in-memory store as they happen; this just flushes the whole default config to disk once. A failed write is logged rather than raised: settings are not worth aborting a session load or a settings dialog over. The error is logged against the path from _get_default_config() directly, since the config_file property falls back to a path helper that creates files on disk, which must not run from an error handler.

Source code in pyrevitlib/pyrevit/userconfig.py
def save_changes(self):
    """Flush the in-memory config to disk.

    Typed and dynamic section edits are already written through to the
    in-memory store as they happen; this just flushes the whole default
    config to disk once. A failed write is logged rather than raised:
    settings are not worth aborting a session load or a settings dialog
    over. The error is logged against the path from
    _get_default_config() directly, since the config_file property falls
    back to a path helper that creates files on disk, which must not run
    from an error handler.
    """
    if not self.is_readonly:
        try:
            self.config_service.Configuration.SaveConfiguration()
        except Exception as save_err:
            mlogger.error(
                "Can not save user config to: %s | %s",
                self._get_default_config().ConfigurationPath,
                save_err,
            )

        self._update_env()
    else:
        mlogger.debug("Config is in admin mode. Skipping save.")
reload()

Reload configuration from disk, discarding unsaved in-memory edits.

Drops the cached shared service so the next access re-reads from disk across every consumer, not just this object.

Source code in pyrevitlib/pyrevit/userconfig.py
def reload(self):
    """Reload configuration from disk, discarding unsaved in-memory edits.

    Drops the cached shared service so the next access re-reads from
    disk across every consumer, not just this object.
    """
    PyRevit.PyRevitConfigs.ReloadConfig()
get_list_separator() staticmethod

Get list separator defined in user os regional settings.

Source code in pyrevitlib/pyrevit/userconfig.py
@staticmethod
def get_list_separator():
    """Get list separator defined in user os regional settings."""
    intkey = coreutils.get_reg_key(
        wr.HKEY_CURRENT_USER, r"Control Panel\International"
    )
    if intkey:
        try:
            return wr.QueryValueEx(intkey, "sList")[0]
        except Exception:
            return DEFAULT_CSV_SEPARATOR
has_section(section_name)

Check if the config contains the given section.

Parameters:

Name Type Description Default
section_name str

name of the section

required

Returns:

Type Description
bool

whether the section exists

Source code in pyrevitlib/pyrevit/userconfig.py
def has_section(self, section_name):
    """Check if the config contains the given section.

    Args:
        section_name (str): name of the section

    Returns:
        (bool): whether the section exists
    """
    return self.config_sections.has_section(section_name)
add_section(section_name)

Add a new section to the config.

Parameters:

Name Type Description Default
section_name str

name of the section

required

Returns:

Type Description
ConfigSection

the added section

Source code in pyrevitlib/pyrevit/userconfig.py
def add_section(self, section_name):
    """Add a new section to the config.

    Args:
        section_name (str): name of the section

    Returns:
        (ConfigSection): the added section
    """
    return self.config_sections.add_section(section_name)
get_section(section_name)

Get the named config section.

Parameters:

Name Type Description Default
section_name str

name of the section

required

Returns:

Type Description
ConfigSection

the requested section

Raises:

Type Description
AttributeError

if the section does not exist

Source code in pyrevitlib/pyrevit/userconfig.py
def get_section(self, section_name):
    """Get the named config section.

    Args:
        section_name (str): name of the section

    Returns:
        (ConfigSection): the requested section

    Raises:
        AttributeError: if the section does not exist
    """
    return self.config_sections.get_section(section_name)
remove_section(section_name)

Remove the named section, and its subsections, from the config.

Parameters:

Name Type Description Default
section_name str

name of the section

required
Source code in pyrevitlib/pyrevit/userconfig.py
def remove_section(self, section_name):
    """Remove the named section, and its subsections, from the config.

    Args:
        section_name (str): name of the section
    """
    self.config_sections.remove_section(section_name)

Functions: