Revit Version Check - Implementation

Hi,

I’ve been using pyRevit for a while and love it! The work you guys are doing is outstanding.

I’ve been wrestling with a Revit version issue for a while:
I’m maintaining a python library for Revit up to Revit 2024 and a new version of the same library for Revit 2025 onwards. In addition some of my pyRevit add-ins are written in c# and are targeting roughly the same version bands.

I just forked pyRevit and got Claude to check the loader code re version enforcement and it didn’t come up with any hits. Reading the version of an extension, tab, panel, button yes, but enforcing seems to be not present. Is that finding of Claude correct?

If so I wouldn’t mind having a go at it based on this doc (please ignore Claude’s tone…)

pyRevit: Revit Version Filtering for Extensions

Background

This document summarises an analysis of the current pyRevit loader architecture with respect to `min_revit_version` / `max_revit_version` support, and proposes a design for enforcing those constraints at parse time in the C# extension parser.


Current State: What Works and What Doesn’t

What is already implemented

The data model is fully in place. Both `bundle.yaml` and Python script constants support `min_revit_version` and `max_revit_version` declarations. These are parsed correctly by `BundleParser.cs` and `ExtensionParser.cs`, with a clear precedence rule: **bundle.yaml takes priority over script-level constants**. The values are stored on `ParsedBundle`, `ParsedComponent`, and `ParsedExtension` — they flow through the entire parsed hierarchy correctly.

What is missing

**Neither the C# loader nor the Python session manager ever reads these fields back to make a load/skip/disable decision.**

The full loading chain was traced:

  • `PyRevitLoaderApplication.cs` (`OnStartup`) — bootstraps IronPython and fires `pyrevitloader.py`. Does not check version constraints.
  • `pyrevitloader.py` — calls `sessionmgr.load_session()`. No filtering here.
  • `sessionmgr._new_session()` — loops over all installed UI extensions, calls `asmmaker.create_assembly()` then `uimaker.update_pyrevit_ui()`. No version check in this loop.
  • `uimaker._produce_ui_pushbutton()` — the function that actually registers a button with the Revit ribbon. Has two early-exit guards: `is_supported` (component type support, unrelated to Revit version) and `is_beta` (work-in-progress flag). Neither touches `min_revit_version` or `max_revit_version`.
  • `systemdiag.check_min_host_version()` — checks a global build number from the *user’s pyRevit config*, not per-component version metadata. Logs a warning only; does not abort loading.

**Result:** A button declaring `min_revit_version: 2025` will be loaded, registered, and appear in the Revit ribbon on Revit 2022 without any warning. The version constraint fields are currently documentation-only metadata.


The Component Hierarchy

The parser builds a nested tree that maps directly to the Revit UI:

```
ParsedExtension (.extension folder)
└── ParsedComponent (.tab)
└── ParsedComponent (.panel)
└── ParsedComponent (.stack / .pulldown / .splitbutton)
└── ParsedComponent (.pushbutton / .smartbutton / etc.)
```

Each level is a `ParsedComponent` (or `ParsedExtension` at the root), and each level can independently declare `MinRevitVersion` / `MaxRevitVersion` in its own `bundle.yaml`. There is no inheritance — a version constraint on a panel does not automatically propagate to its child buttons. Instead, pruning a container naturally removes all its children because they are never visited.


Proposed Design

Guiding decisions

  • **Missing constraint = valid for all versions.** Null or empty `min_revit_version` / `max_revit_version` means no restriction. Only explicit declarations are enforced.
  • **Empty containers** (e.g. a panel whose buttons were all pruned) are already handled by the existing `uimaker` logic, which calls `deactivate()` on containers with zero surviving children. No change needed there.
  • **Revit year** is passed as a new integer parameter into the parser entry point, rather than being read from a global or static. `PyRevitLoaderApplication` already reads `VersionNumber` from the Revit API — it is the natural place to obtain and supply this value.

The version check logic

For a component with a given `revitYear` integer:

  • Passes if `MinRevitVersion` is null/empty, OR `revitYear >= int(MinRevitVersion)`
  • AND `MaxRevitVersion` is null/empty, OR `revitYear <= int(MaxRevitVersion)`

Both conditions must hold. The year strings are always four-digit integers (e.g. `“2024”`), so integer parsing is safe and unambiguous.

Two insertion points in `ExtensionParser.cs`

**1. Extension level — inside `ParseExtension()`**

Immediately after reading `bundle.yaml` and before the call to `ParseComponents()`, check the extension’s `MinRevitVersion` / `MaxRevitVersion` against `revitYear`. If the extension is out of range, return null / skip the yield. The entire directory tree is never traversed — this is the cheapest possible gate.

**2. Component level — inside `ParseComponents()`**

This is the recursive function that builds the `children` list at every level (tabs, panels, stacks, pulldowns, buttons). At the point where each component is about to be added to the result list, apply the same version check. If the component fails, it is not added, and because `ParseComponents()` is already recursive, this single check covers all levels uniformly. The children of a pruned component are simply never visited.

What does not need to change

  • `ParsedComponent`, `ParsedBundle`, and `ParsedExtension` data models — already correct, no changes needed.
  • The uimaker — empty container deactivation already exists.
  • The Python session path (`_new_session` via `extensionmgr`) — separate from this C# parser, unaffected.
  • The C# session path (`_new_session_csharp`) — calls `PyRevitLoaderApplication.LoadSession()`, which already reads `VersionNumber` from Revit. This is where `revitYear` would be extracted and passed into `ParseInstalledExtensions()`.

Parameter threading

`revitYear` enters at `ParseInstalledExtensions()` (the public entry point called by `SessionManagerService`) and is threaded through:

```
ParseInstalledExtensions(revitYear)
→ ParseExtension(revitYear) [extension-level gate]
→ ParseComponents(revitYear) [recursive component-level gate]
```


One Edge Case Worth Noting

Revit ribbon stacks have a fixed slot count (typically 3). If version filtering removes one or two buttons from a stack but leaves others, the stack may render with gaps. This is a pre-existing quirk of the Revit ribbon API rather than a parser problem, and is consistent with the decision to leave empty-container handling to the uimaker layer. It is noted here for awareness rather than as a blocker.


Summary

Layer Current state Proposed change
`BundleParser.cs` Parses min/max version correctly No change
`ExtensionParser.cs` Stores min/max version on components Add version check at extension level and inside recursive `ParseComponents()`
`PyRevitLoaderApplication.cs` Reads Revit `VersionNumber` for other purposes Supply `revitYear` as parameter to `ParseInstalledExtensions()`
`uimaker.py` Deactivates empty containers No change
`sessionmgr.py` No version filtering No change (handled upstream in C# parser)

EDIT: open issue via:

Just tick off the box “use new loader” - only drawback is slower startup time.

@pyrevti Thank you for pointing me towards the issue on GitHub. I’m having a go at a patch …see how I go.

As a side: I had issues with version filtering in pyRevit 5.x. At least at extension level I could not get it to work.