Bundles
Anatomy of an extension
The four folder levels, how discovery walks them, and the ordering and naming rules that decide what your ribbon looks like.
An extension is a folder tree with meaningful suffixes. pyNavis reads that tree at startup and on every Reload, and builds the ribbon from it. Nothing else registers a command: there is no manifest, no index file, and no place to list your tools. If the folders are right the buttons exist, and if a button is missing the folders are wrong.
This page is the reference for that tree. It covers every level, the two configuration files that sit outside a bundle, the ordering rules, how a folder name becomes a title, and every reason discovery drops something.
The four levels#
Each level is a folder whose name ends in a fixed suffix. Matching is case-insensitive, so
Survey.Tab and Survey.tab are the same thing, but the suffix itself
is matched literally: nothing else counts.
| Suffix | Lives under | Becomes | May contain |
|---|---|---|---|
*.extension | An extension root | Nothing visible; it is the unit of distribution | extension.yaml, lib\, hooks\,
startup.py, *.tab |
*.tab | *.extension | One ribbon tab | bundle.yaml (a layout: list only), *.panel |
*.panel | *.tab | One panel inside that tab | bundle.yaml (a layout: list only), *.pushbutton,
*.stack, *.pulldown, and nine more panel item suffixes (see
below) |
*.pushbutton | *.panel, *.stack, *.pulldown |
One clickable button | script.py (required), config.py, bundle.yaml, icons |
*.stack | *.panel | 2 or 3 small buttons in one column | bundle.yaml (a layout: list only), *.pushbutton |
*.pulldown | *.panel | One large button that opens a menu | bundle.yaml, icons, *.pushbutton |
*.slideout | *.panel |
Nothing of its own; its contents render below a panel break, in the flyout the host opens from the panel title | bundle.yaml (a layout: list only), any of the other panel
item suffixes, one level deep |
The table above covers the three shapes you will use constantly, but a panel understands
nine more: *.nobutton, *.urlbutton, *.linkbutton,
*.toggle, *.smartbutton, *.splitbutton,
*.splitpushbutton, *.dockpane and *.slideout.
Buttons, stacks and pulldowns covers all twelve. If you have come
from pyRevit, note the exact spelling: it is *.nobutton, not
*.nobundle, and *.toggle, not *.togglebutton.
A folder inside a panel whose name contains a dot is taken to be an attempted bundle, so a
suffix outside the twelve is logged as '<dir>' is not a bundle kind pyNavis knows
(check the folder suffix) - skipped. and counted into the warning toast pyNavis shows
after every scan: N bundle problems found, quoting the first one. A folder with
no dot in its name, such as assets or __pycache__, is an ordinary
folder and says nothing.
A complete extension#
Everything below is either required, optional, or ignored. Nothing is boilerplate.
D:\NavisTools\ # an extension root, listed in config.json
Shared.lib\ # optional: a sibling of *.extension, not inside one
acme_common\ # shared python for every extension, every engine
__init__.py
Survey.extension\
extension.yaml # optional: display name, default engine, tab order
startup.py # optional: runs at boot and every Reload
lib\ # optional: modules importable from every button IN
surveyutil\ # this extension only - different from Shared.lib\ above
__init__.py
geometry.py
hooks\ # optional: event scripts, filename = event name
doc-opened.py
Survey.tab\ # one ribbon tab
bundle.yaml # optional: layout: Export, Review
Export.panel\
bundle.yaml # optional: layout: Points, Mesh, Format, More
Points.pushbutton\ # large button, titled "Points"
script.py # REQUIRED, or the whole bundle is dropped
config.py # optional: Shift+Click runs this
bundle.yaml # optional: title, tooltip, engine, shortcut, keytip
icon.png # 96x96, light theme, large slot
icon.dark.png # 96x96, dark theme
icon.small.png # 32x32, stacks and menu rows
icon.small.dark.png # 32x32, dark theme
Mesh.pushbutton\
script.py
Format.stack\ # 2 or 3 children; no icon of its own
bundle.yaml # optional: layout: CSV, IFC
CSV.pushbutton\
script.py
icon.small.png
IFC.pushbutton\
script.py
icon.small.png
More.pulldown\ # a menu button; has its own yaml and icon
bundle.yaml # title, tooltip, keytip, layout
icon.png
Legacy.pushbutton\
script.py
Debug.pushbutton\
script.py
Extras.slideout\ # contents move below the panel break, into the flyout
Settings.pushbutton\
script.py
Review.panel\
Compare.pushbutton\ # no yaml: titled "Compare"
script.pyThat tree produces one tab named Survey with two panels. The Export panel
holds four items in the order its layout: list gives: a large button, a large
button, a two-row stack and a pulldown, plus one more button hidden in the flyout the panel
title opens. The Review panel holds one button and needs no list.
How discovery walks the disk#
Discovery is a fixed six-stage walk. Every stage globs for one pattern and ignores everything else it finds, which is why a misplaced folder produces no error: it was never looked at.
Where roots come from#
An extension is only found if it sits directly inside a scanned root. The
roots are the strings in the "extensions" array of
%APPDATA%\pyNavis\config.json, plus two defaults added when they are not already
in the array: %APPDATA%\pyNavis\extensions, which is where the install puts the
shipped extension, and %PROGRAMDATA%\pyNavis\extensions if that folder exists
and only administrators can write to it, which is how extensions are deployed to a whole
machine. Any user can create folders under %PROGRAMDATA%, so a machine-wide root
others could write to is skipped with a log line: its hooks and startup.py would
otherwise run for everyone on the PC. All roots are scanned and their
results concatenated, so a development extension and an installed one happily coexist.
D:\NavisTools\ Survey.extension\ Drawings.extension\
D:\NavisTools\
Team\
Survey.extension\ # never found
Archive\
Drawings.extension\ # never foundIf you want to group extensions in subfolders, add each subfolder as its own root instead:
pynavis extensions add D:\NavisTools\Team.
extension.yaml#
Optional, and it understands exactly three keys. Anything else in the file is ignored.
name: Survey Tools
engine: ironpython
layout:
- Survey| Key | Effect | Default |
|---|---|---|
name |
The extension's display name. It is not shown on the ribbon; it is one half of every tab id this extension creates. | The folder name with .extension removed |
engine |
The default engine for every bundle in the extension. A bundle's own
engine: still wins. |
ironpython |
layout |
The order of the extension's tabs, as a block list of *.tab folder
names. See Ordering panels and buttons. |
Tab folders in name order |
Setting engine: cpython here is the clean way to write a whole extension
against CPython without repeating the key in every bundle.yaml. The
layout: list only matters once an extension has more than one tab.
The lib folder (one extension)#
A folder named lib directly inside the .extension folder is added
to the module search path of every button in that extension. It is the place for shared code:
helpers, vendored pure-Python packages, anything more than one tool imports.
"""Exports survey points."""
from surveyutil import points # resolved from Survey.extension\lib
from pynavis import selection, toast
toast.info('%d point(s)' % len(points.of_items(selection.get_items())))Each button gets two search paths, in this order:
- its own bundle folder, so a module beside
script.pyshadows the extension'slib\; - the extension's
lib\folder, if it exists.
A lib folder inside a .tab, a .panel, a
.stack or a bundle is an ordinary folder and is never added to the search path.
There is exactly one lib per extension and it lives beside
extension.yaml.
*.lib extensions (every extension, both engines)#
A different thing with a similar name: a folder ending in *.lib sitting
directly under an extension root, a sibling of your *.extension folders rather
than something inside one. Its contents join the global module search path shared by
every extension and both engines, not just the buttons in
one extension.
D:\NavisTools\ # an extension root
Shared.lib\ # a library-only extension, not a *.extension
acme_common\
__init__.py
units.py
Survey.extension\
...
Drawings.extension\
... # both can "import acme_common"Search path order puts every *.lib folder ahead of the bundle folder and the
per-extension lib\, right after pynavislib:
pynavislib, then every *.lib folder found (in sorted order), then
the bundle folder, then that extension's own lib\.
The set of *.lib folders is fixed into the engine configuration once, at boot.
Reload rescans it and, if the set changed, toasts Library extensions changed - Added or
removed *.lib folders apply after a Navisworks restart. and leaves the old set in place.
Edits to files already inside an existing *.lib folder are a different matter:
they need a Reload, not a restart. A Reload drops the cached modules under every known
*.lib folder, and under pynavislib, so the next run picks up your
change. Only a module in your own bundle folder or your extension's lib\ is
evicted before every run.
startup.py#
A file named exactly startup.py directly inside a .extension
folder runs once at boot and again at the start of every Reload, before that extension's
ribbon is built.
Survey.extension\
startup.py
extension.yaml"""Confirms the shared geometry helper resolved before anyone clicks a button."""
from pynavis import toast
try:
import surveyutil
except ImportError:
toast.warning('surveyutil not found', 'Check Survey.extension\\lib\\surveyutil.')startup.py runs on the extension's default engine with the globals a hook
gets, minus __event__: __file__, __pynavis__ and
__runner__, which is True only when
pynavis run starts it. It gets the same search
paths: the extension folder itself, then its lib\ if it has one, plus the global
*.lib folders every script gets. Output is routed to the pyNavis log rather than
a window, because there is no button behind it to title one after, and every line is labelled
with what is actually running ([startup Survey Tools]) rather than with the last
command clicked, which on a Reload would be Reload itself.
An exception in startup.py is logged and toasted, but discovery carries on and
that extension's ribbon still builds. A broken startup.py costs you whatever it
was supposed to set up, not the whole extension.
startup.py failed for 'Survey Tools':
Traceback (most recent call last):
...pynavis.script.get_toggle_state/set_toggle_state act on
"whichever bundle is currently running", which only means something inside that bundle's own
script.py or config.py. Neither startup.py nor a hook
runs as any particular bundle, so there is no supported way to flip a specific
*.toggle's state from either of them. If a toggle needs to remember its setting
across a restart, store the flag with pynavis.settings and have the toggle's own
script.py read it on the very first click of the session.
Hooks covers the companion mechanism: event scripts under
hooks\ that run later, in response to something happening, rather than once at
boot or Reload.
Ordering panels and buttons#
At every level, order comes from a layout: list in the container's own
configuration file: extension.yaml for the tabs of an extension, and a
bundle.yaml inside a .tab, a .panel, a
.stack, a .pulldown, a .splitbutton, a
.splitpushbutton or a .slideout for the folders inside it. The list
names the child folders in the order you want them. There is no order: key on
the children themselves, and the order you created the folders in is irrelevant.
layout:
- Remember
- Recall
- Set
- Step
- MoreSelection.panel\
bundle.yaml
Remember.pushbutton\ # first, as the list says
Recall.pushbutton\
Set.stack\
Step.stack\
More.pulldown\ # last, as the list saysThe rules are short:
- Listed children come first, in list order.
- Unlisted children follow, in case-insensitive name order, so a folder you forgot to list is never hidden. It just lands after the ones you did list.
- An entry may name the folder with or without its kind suffix:
RecallandRecall.pushbuttonboth match. Matching ignores case, and it ignores a legacyNN_prefix on either side. - An entry that names no folder in that container is logged, counted into the extension's
problem list on the same channel as a mistyped suffix, and skipped:
The layout in 'D:\NavisTools\...\Selection.panel' names 'Recal', but no such folder is there - ignored.
Inserting a tool in position three is therefore one new line in the list. Nothing gets renamed, and nothing else moves.
A panel does not group buttons, stacks and pulldowns. All of them are placed by the same list, so a stack can sit between two large buttons simply by listing it there. Buttons, stacks and pulldowns shows this on a real panel.
Before layout: existed, folders were ordered by name and carried a prefix of
two or more digits and an underscore, as in 01_Remember.pushbutton, which
sorted the folder and was stripped from the title. That still works, and an extension written
that way loads unchanged: without a layout: list the folders sort by name,
prefix included, and the prefix never reaches the title or the bundle key. New extensions
should use the list. The shipped extension no longer uses prefixes at all.
From folder name to title#
When a bundle has no title: in its bundle.yaml and no
__title__ in its script, the title is derived from the folder name by two rules
applied in order: strip a leading run of two or more digits followed by an
underscore, which is the legacy ordering prefix older extensions still carry, then turn every
remaining underscore into a space.
1_Keep keeps its name. If you want a single digit in the label, set
title: and stop guessing. A new extension has no reason to carry a prefix at all:
order it with layout: and name the folder what the button should say.Titles are only derived when nothing better is available. The full precedence, most
specific first, is bundle.yaml, then the script's __title__ or
docstring, then the folder name. bundle.yaml covers every key.
When a bundle does not appear#
Discovery never throws. It drops what it cannot use and carries on, which keeps one broken
folder from taking down the whole ribbon. Most drops announce themselves twice: once in
%APPDATA%\pyNavis\logs\pyNavis.log, and once in a warning toast after the scan
that counts them and quotes the first. This table maps what you see to what went wrong, with
the line to search for in the log.
| Symptom | Cause | Log line |
|---|---|---|
| One button missing | The .pushbutton folder has no script.py |
'<dir>' has no script.py - skipped. |
| One button missing, suffix looks fine to you | The suffix is not one of the twelve, for example .pushbuttons |
'<dir>' is not a bundle kind pyNavis knows (check the folder suffix) -
skipped. |
| A dock panel's ribbon toggle missing | The .dockpane folder has no pane.xaml |
Dockpane '<dir>' has no pane.xaml - skipped. |
| A whole stack missing | The .stack ended up with fewer than 2 or more than 3 usable buttons,
often because one child lost its script.py |
Stack '<dir>' holds N button(s); a stack needs 2 or 3 - skipped. |
| A pulldown missing | No child .pushbutton had a script.py |
Pulldown '<dir>' has no usable pushbuttons - skipped. |
| A panel missing | Every item in it was skipped, so the panel has zero items | None for the panel itself; the item skips above explain it. |
| A tab missing | Every panel in it was skipped, so the tab has zero panels | None for the tab itself. |
| The second copy of a tab missing | Another extension already produced a tab with the same id | Ribbon tab '<id>' already exists - skipped (duplicate extension?). |
| A stack or pulldown child missing | It is not a *.pushbutton; those two containers hold nothing else |
'<dir>' is ignored: a stack holds only *.pushbutton folders. |
| A slideout's contents missing | The .slideout sits inside another .slideout |
Slideout '<dir>' sits inside another slideout - skipped. |
| Nothing at all from an extension folder | A misspelled .extension, .tab or .panel suffix,
or the extension is not directly inside a scanned root |
None. Those three levels are globbed by pattern, so the walk never looked at the folder and has nothing to report. |
Every successful build logs Ribbon built: N tab(s), M button(s). If M is lower
than you expect, something above is the reason. Troubleshooting
walks through the rest.
Slideouts#
A *.slideout folder is not a bundle and draws nothing itself. It is a marker:
whatever panel items it contains are emitted after a panel break, so the host hides them until
the user clicks the panel title (the little Title with a chevron along the bottom of
the panel), exactly like the native Tags panel. It is where the tools that matter least go,
without their costing any ribbon width.
Export.panel\
bundle.yaml # layout: Points, Mesh
Points.pushbutton\ # always visible
Mesh.pushbutton\ # always visible
Extras.slideout\ # everything below here is in the flyout
bundle.yaml # layout: Settings, Docs
Settings.pushbutton\
script.py
Docs.urlbutton\
bundle.yaml- A slideout may hold any of the other panel item kinds, not just pushbuttons: stacks, pulldowns, toggles, dockpanes and the rest all work.
- A panel may have more than one
.slideout. Their contents are concatenated into the single flyout in the order the panel'slayout:list gives them; there is only ever one break per panel. - The folder name is never shown, so name it for your own benefit. Its own
bundle.yamlholds alayout:list for the items inside it and nothing else; where the slideout itself sits in the panel's list does not matter, because its contents always render after the break. - One level only. A
.slideoutinside a.slideoutis logged (Slideout '<dir>' sits inside another slideout - skipped.) and its contents are dropped. - A panel whose items are all in a slideout still renders: the panel exists, the visible area is empty, and everything is in the flyout.
- Buttons in a slideout are ordinary buttons in every other respect. They take shortcuts,
keytips,
context:gating and bundle keys exactly like the visible ones, and their bundle key keeps the.slideoutfolder in the path.
Names and identity#
Two identities are derived from the tree, and both matter when you ship an extension to other people.
Tab id#
Every ribbon tab needs a unique id. pyNavis builds one from the extension name and the tab folder name, replacing every character that is not a letter or digit with an underscore.
Survey-Tools and
Survey Tools produce identical ids, so pick names that differ in more than
punctuation.The practical rule: give your extension a name nobody else will use. If a colleague installs a second copy of your extension in a different root, the second copy is not loaded at all and the log says so, rather than every button being drawn twice.
Adding to someone else's tab or panel#
Tabs, and panels inside them, join by name across extensions: a pyNavis.tab
in your extension adds to the pyNavis tab instead of drawing a second one, and a
Clash.panel inside it adds your buttons to the end of the Clash panel. The
matching ignores case. A shared tab stays where the first extension in load order puts it,
the shipped pyNavis extension's panels and buttons come first in anything it shares, and
other extensions follow in load order. The tab keeps the first contributor's id.
%APPDATA%\pyNavis\extensions\
pyNavis.extension\ # the installer's; every update replaces it
My pyNavis.extension\ # yours; updates never touch it
pyNavis.tab\
Clash.panel\
Tag Clashes.pushbutton\ # lands at the end of the pyNavis Clash panel
Team.panel\
Publish.pushbutton\ # a new panel on the pyNavis tabThis is why nothing of yours belongs inside pyNavis.extension: the installer
replaces that folder on every update. It backs up what it finds there and moves the buttons
and panels you added into My pyNavis.extension first, but a file added inside
one of pyNavis's own buttons, or an edit to one of its files, can only go to the backup.
Bundle key#
Each button also carries a stable key: its path relative to the .extension
folder, with forward slashes, keeping every suffix. A legacy NN_ prefix is
dropped from every segment.
pyNavis.tab/Selection.panel/Remember.pushbutton
pyNavis.tab/Selection.panel/Set.stack/Add.pushbuttonThat key is what config.json uses to rebind or disable a tool's keyboard
shortcut and to record which slot a *.dockpane claimed, so renaming a folder,
meaning the name after any prefix, silently orphans a user's override.
Shortcuts covers the binding file.
pyNavis.tab/02_Selection.panel/01_Remember.pushbutton and
pyNavis.tab/Selection.panel/Remember.pushbutton are the same key, and a key
written into config.json with prefixes is normalised on load. A user's shortcut
overrides and dock-pane slot claims survive an author adding, changing or removing prefixes.
Renaming the folder name itself, Remember to Memorize, is still a
new key.
What to check when you add a bundle#
- The extension folder sits directly in a root from
config.json. - Every suffix is spelled correctly and appears exactly once per folder name.
- Every
.pushbuttoncontains ascript.py. - Every
.stackcontains 2 or 3 of them. - Every entry in a
layout:list names a folder that exists, spelled the way the folder is. - You clicked Reload.