diff --git a/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll b/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll index 850160317..01f0d8731 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll and b/bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll b/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll index f952b8920..0e7047a22 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll and b/bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll index a0e343b8a..9163c2159 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2025.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll index 414100956..5a19b2b2a 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2026.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2027.dll b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2027.dll index f33e6a7fd..2fa749818 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2027.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2027.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll b/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll index 90181e4af..56ea1e900 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll and b/bin/netcore/engines/IPY2712PR/pyRevitLoader.dll differ diff --git a/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll b/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll index d86e77365..77f10986e 100644 Binary files a/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll and b/bin/netcore/engines/IPY2712PR/pyRevitRunner.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll b/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll index 850160317..01f0d8731 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll and b/bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll b/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll index f952b8920..0e7047a22 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll and b/bin/netcore/engines/IPY342/pyRevitExtensionParser.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll index 6d3e25616..2dd53b381 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll and b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2025.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll index 232e918ba..465b9080c 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll and b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2026.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2027.dll b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2027.dll index d3cfe63cb..3c0d7dcf0 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2027.dll and b/bin/netcore/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2027.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitLoader.dll b/bin/netcore/engines/IPY342/pyRevitLoader.dll index 19f5df0f6..3a2165f08 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitLoader.dll and b/bin/netcore/engines/IPY342/pyRevitLoader.dll differ diff --git a/bin/netcore/engines/IPY342/pyRevitRunner.dll b/bin/netcore/engines/IPY342/pyRevitRunner.dll index 590ca6b91..e2b4cede1 100644 Binary files a/bin/netcore/engines/IPY342/pyRevitRunner.dll and b/bin/netcore/engines/IPY342/pyRevitRunner.dll differ diff --git a/bin/netcore/pyRevitLabs.Common.dll b/bin/netcore/pyRevitLabs.Common.dll index 8f1f2feee..5d195387d 100644 Binary files a/bin/netcore/pyRevitLabs.Common.dll and b/bin/netcore/pyRevitLabs.Common.dll differ diff --git a/bin/netcore/pyRevitLabs.CommonCLI.dll b/bin/netcore/pyRevitLabs.CommonCLI.dll index bb357839a..5df1ef211 100644 Binary files a/bin/netcore/pyRevitLabs.CommonCLI.dll and b/bin/netcore/pyRevitLabs.CommonCLI.dll differ diff --git a/bin/netcore/pyRevitLabs.CommonWPF.dll b/bin/netcore/pyRevitLabs.CommonWPF.dll index 814726333..c2fd0b1f0 100644 Binary files a/bin/netcore/pyRevitLabs.CommonWPF.dll and b/bin/netcore/pyRevitLabs.CommonWPF.dll differ diff --git a/bin/netcore/pyRevitLabs.DeffrelDB.dll b/bin/netcore/pyRevitLabs.DeffrelDB.dll index 69d465465..f57ca7487 100644 Binary files a/bin/netcore/pyRevitLabs.DeffrelDB.dll and b/bin/netcore/pyRevitLabs.DeffrelDB.dll differ diff --git a/bin/netcore/pyRevitLabs.Emojis.dll b/bin/netcore/pyRevitLabs.Emojis.dll index 1ee0a91c5..64cbbda6d 100644 Binary files a/bin/netcore/pyRevitLabs.Emojis.dll and b/bin/netcore/pyRevitLabs.Emojis.dll differ diff --git a/bin/netcore/pyRevitLabs.Language.dll b/bin/netcore/pyRevitLabs.Language.dll index 0ccc3b4c0..7823cbd0e 100644 Binary files a/bin/netcore/pyRevitLabs.Language.dll and b/bin/netcore/pyRevitLabs.Language.dll differ diff --git a/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll b/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll index a00656853..33280879c 100644 Binary files a/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll and b/bin/netcore/pyRevitLabs.PyRevit.Runtime.Shared.dll differ diff --git a/bin/netcore/pyRevitLabs.PyRevit.dll b/bin/netcore/pyRevitLabs.PyRevit.dll index 5218cb73c..a8ec01e3b 100644 Binary files a/bin/netcore/pyRevitLabs.PyRevit.dll and b/bin/netcore/pyRevitLabs.PyRevit.dll differ diff --git a/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll b/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll index 1ff159982..4109bee72 100644 Binary files a/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll and b/bin/netcore/pyRevitLabs.TargetApps.AutoCAD.dll differ diff --git a/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll b/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll index 3f82d477a..78b6c7fa3 100644 Binary files a/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll and b/bin/netcore/pyRevitLabs.TargetApps.Navisworks.dll differ diff --git a/bin/netcore/pyRevitLabs.TargetApps.Revit.dll b/bin/netcore/pyRevitLabs.TargetApps.Revit.dll index 9dd0dad10..a1efe9f2b 100644 Binary files a/bin/netcore/pyRevitLabs.TargetApps.Revit.dll and b/bin/netcore/pyRevitLabs.TargetApps.Revit.dll differ diff --git a/bin/netcore/pyRevitLabs.UnitTests.dll b/bin/netcore/pyRevitLabs.UnitTests.dll index dccc1f014..364ea275e 100644 Binary files a/bin/netcore/pyRevitLabs.UnitTests.dll and b/bin/netcore/pyRevitLabs.UnitTests.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll b/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll index 0ecad3b1d..0d90c480e 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll and b/bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll b/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll index be5087f3d..49261c7c1 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll and b/bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll index c327a57a8..7a092e1cb 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2017.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll index f4b5f18cf..36b1d4f2b 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2018.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll index 0e12279e9..ccbcb7a07 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2019.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll index 21256bfd8..f65408b97 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2020.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll index d042c9117..354368ddb 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2021.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll index 7487d1323..c7129809a 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2022.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll index 720ed845f..f65111742 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2023.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll index 105602190..e24da8ea1 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLabs.PyRevit.Runtime.2024.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll b/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll index b515fe5fb..0dab26ecf 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll and b/bin/netfx/engines/IPY2712PR/pyRevitLoader.dll differ diff --git a/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll b/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll index 3d56861a4..b820ece98 100644 Binary files a/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll and b/bin/netfx/engines/IPY2712PR/pyRevitRunner.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll b/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll index 0ecad3b1d..0d90c480e 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll and b/bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll b/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll index be5087f3d..49261c7c1 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll and b/bin/netfx/engines/IPY342/pyRevitExtensionParser.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll index 15af0cb20..8dd40147d 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2017.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll index c2b31a808..3cffccaf9 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2018.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll index 664afc5cd..ebdf3ed97 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2019.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll index 10856b61e..f351aa528 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2020.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll index 7c558347d..7efde9ced 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2021.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll index e8463cb28..1205aadf4 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2022.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll index da3beca0e..648ef9ca5 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2023.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll index 620615ffa..46d3ceecf 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll and b/bin/netfx/engines/IPY342/pyRevitLabs.PyRevit.Runtime.2024.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitLoader.dll b/bin/netfx/engines/IPY342/pyRevitLoader.dll index 892ec93ea..670dd90b0 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitLoader.dll and b/bin/netfx/engines/IPY342/pyRevitLoader.dll differ diff --git a/bin/netfx/engines/IPY342/pyRevitRunner.dll b/bin/netfx/engines/IPY342/pyRevitRunner.dll index 6acaa346f..3faffff23 100644 Binary files a/bin/netfx/engines/IPY342/pyRevitRunner.dll and b/bin/netfx/engines/IPY342/pyRevitRunner.dll differ diff --git a/bin/netfx/pyRevitLabs.Common.dll b/bin/netfx/pyRevitLabs.Common.dll index a38082617..42310c07e 100644 Binary files a/bin/netfx/pyRevitLabs.Common.dll and b/bin/netfx/pyRevitLabs.Common.dll differ diff --git a/bin/netfx/pyRevitLabs.CommonCLI.dll b/bin/netfx/pyRevitLabs.CommonCLI.dll index 5293ecee5..20d1584dd 100644 Binary files a/bin/netfx/pyRevitLabs.CommonCLI.dll and b/bin/netfx/pyRevitLabs.CommonCLI.dll differ diff --git a/bin/netfx/pyRevitLabs.CommonWPF.dll b/bin/netfx/pyRevitLabs.CommonWPF.dll index 37aaa4e53..38c31fbf3 100644 Binary files a/bin/netfx/pyRevitLabs.CommonWPF.dll and b/bin/netfx/pyRevitLabs.CommonWPF.dll differ diff --git a/bin/netfx/pyRevitLabs.DeffrelDB.dll b/bin/netfx/pyRevitLabs.DeffrelDB.dll index 2c7ef5e06..8420d0f88 100644 Binary files a/bin/netfx/pyRevitLabs.DeffrelDB.dll and b/bin/netfx/pyRevitLabs.DeffrelDB.dll differ diff --git a/bin/netfx/pyRevitLabs.Emojis.dll b/bin/netfx/pyRevitLabs.Emojis.dll index 4254e4fdd..f77af4950 100644 Binary files a/bin/netfx/pyRevitLabs.Emojis.dll and b/bin/netfx/pyRevitLabs.Emojis.dll differ diff --git a/bin/netfx/pyRevitLabs.Language.dll b/bin/netfx/pyRevitLabs.Language.dll index c7f4fc66d..183d2028a 100644 Binary files a/bin/netfx/pyRevitLabs.Language.dll and b/bin/netfx/pyRevitLabs.Language.dll differ diff --git a/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll b/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll index 43cb3f134..8811df5d3 100644 Binary files a/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll and b/bin/netfx/pyRevitLabs.PyRevit.Runtime.Shared.dll differ diff --git a/bin/netfx/pyRevitLabs.PyRevit.dll b/bin/netfx/pyRevitLabs.PyRevit.dll index f5b35dccc..a14888a10 100644 Binary files a/bin/netfx/pyRevitLabs.PyRevit.dll and b/bin/netfx/pyRevitLabs.PyRevit.dll differ diff --git a/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll b/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll index b00950b65..d54c54c48 100644 Binary files a/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll and b/bin/netfx/pyRevitLabs.TargetApps.AutoCAD.dll differ diff --git a/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll b/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll index c99bf9fb2..ddaa40ec5 100644 Binary files a/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll and b/bin/netfx/pyRevitLabs.TargetApps.Navisworks.dll differ diff --git a/bin/netfx/pyRevitLabs.TargetApps.Revit.dll b/bin/netfx/pyRevitLabs.TargetApps.Revit.dll index 6d328c224..6b277a74a 100644 Binary files a/bin/netfx/pyRevitLabs.TargetApps.Revit.dll and b/bin/netfx/pyRevitLabs.TargetApps.Revit.dll differ diff --git a/bin/netfx/pyRevitLabs.UnitTests.dll b/bin/netfx/pyRevitLabs.UnitTests.dll index 9ffb3a542..0b25dc345 100644 Binary files a/bin/netfx/pyRevitLabs.UnitTests.dll and b/bin/netfx/pyRevitLabs.UnitTests.dll differ diff --git a/bin/pyRevitLabs.Common.dll b/bin/pyRevitLabs.Common.dll index 8f1f2feee..5d195387d 100644 Binary files a/bin/pyRevitLabs.Common.dll and b/bin/pyRevitLabs.Common.dll differ diff --git a/bin/pyRevitLabs.CommonCLI.dll b/bin/pyRevitLabs.CommonCLI.dll index bb357839a..5df1ef211 100644 Binary files a/bin/pyRevitLabs.CommonCLI.dll and b/bin/pyRevitLabs.CommonCLI.dll differ diff --git a/bin/pyRevitLabs.CommonWPF.dll b/bin/pyRevitLabs.CommonWPF.dll index 814726333..c2fd0b1f0 100644 Binary files a/bin/pyRevitLabs.CommonWPF.dll and b/bin/pyRevitLabs.CommonWPF.dll differ diff --git a/bin/pyRevitLabs.Language.dll b/bin/pyRevitLabs.Language.dll index 0ccc3b4c0..7823cbd0e 100644 Binary files a/bin/pyRevitLabs.Language.dll and b/bin/pyRevitLabs.Language.dll differ diff --git a/bin/pyRevitLabs.PyRevit.dll b/bin/pyRevitLabs.PyRevit.dll index 5218cb73c..a8ec01e3b 100644 Binary files a/bin/pyRevitLabs.PyRevit.dll and b/bin/pyRevitLabs.PyRevit.dll differ diff --git a/bin/pyRevitLabs.TargetApps.Revit.dll b/bin/pyRevitLabs.TargetApps.Revit.dll index 9dd0dad10..a1efe9f2b 100644 Binary files a/bin/pyRevitLabs.TargetApps.Revit.dll and b/bin/pyRevitLabs.TargetApps.Revit.dll differ diff --git a/bin/pyrevit-autocomplete.exe b/bin/pyrevit-autocomplete.exe index 7010dbd2f..5da9312fb 100644 Binary files a/bin/pyrevit-autocomplete.exe and b/bin/pyrevit-autocomplete.exe differ diff --git a/bin/pyrevit-doctor.dll b/bin/pyrevit-doctor.dll index b17244355..cc977242e 100644 Binary files a/bin/pyrevit-doctor.dll and b/bin/pyrevit-doctor.dll differ diff --git a/bin/pyrevit-doctor.exe b/bin/pyrevit-doctor.exe index 6ac0b6a33..f5ff41c0e 100644 Binary files a/bin/pyrevit-doctor.exe and b/bin/pyrevit-doctor.exe differ diff --git a/bin/pyrevit.dll b/bin/pyrevit.dll index 77bbf30ad..5ba22756e 100644 Binary files a/bin/pyrevit.dll and b/bin/pyrevit.dll differ diff --git a/bin/pyrevit.exe b/bin/pyrevit.exe index 6c21db7ce..987a48571 100644 Binary files a/bin/pyrevit.exe and b/bin/pyrevit.exe differ diff --git a/dev/pyRevitLabs/pyRevitCLIAutoComplete/pyrevit-autocomplete.go b/dev/pyRevitLabs/pyRevitCLIAutoComplete/pyrevit-autocomplete.go index c1451e8da..21d69dad7 100644 --- a/dev/pyRevitLabs/pyRevitCLIAutoComplete/pyrevit-autocomplete.go +++ b/dev/pyRevitLabs/pyRevitCLIAutoComplete/pyrevit-autocomplete.go @@ -34,8 +34,8 @@ func main() { "env": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--help": complete.PredictNothing, "--json": complete.PredictNothing, + "--help": complete.PredictNothing, "--log": complete.PredictNothing, }, }, @@ -48,12 +48,12 @@ func main() { "clone": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--image": complete.PredictNothing, "--token": complete.PredictNothing, - "--password": complete.PredictNothing, "--dest": complete.PredictNothing, - "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--log": complete.PredictNothing, + "--password": complete.PredictNothing, + "--image": complete.PredictNothing, "--branch": complete.PredictNothing, }, }, @@ -119,16 +119,16 @@ func main() { "origin": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--reset": complete.PredictNothing, "--log": complete.PredictNothing, + "--reset": complete.PredictNothing, }, }, "update": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--password": complete.PredictNothing, "--token": complete.PredictNothing, "--log": complete.PredictNothing, + "--password": complete.PredictNothing, }, }, "deployments": complete.Command{ @@ -149,17 +149,17 @@ func main() { "default": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--allusers": complete.PredictNothing, "--installed": complete.PredictNothing, "--attached": complete.PredictNothing, + "--allusers": complete.PredictNothing, }, }, }, Flags: complete.Flags{ - "--help": complete.PredictNothing, - "--allusers": complete.PredictNothing, "--installed": complete.PredictNothing, "--attached": complete.PredictNothing, + "--help": complete.PredictNothing, + "--allusers": complete.PredictNothing, }, }, "attached": complete.Command{ @@ -186,8 +186,8 @@ func main() { "ui": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--password": complete.PredictNothing, "--token": complete.PredictNothing, + "--password": complete.PredictNothing, "--log": complete.PredictNothing, "--dest": complete.PredictNothing, }, @@ -195,8 +195,8 @@ func main() { "lib": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--password": complete.PredictNothing, "--token": complete.PredictNothing, + "--password": complete.PredictNothing, "--log": complete.PredictNothing, "--dest": complete.PredictNothing, }, @@ -204,10 +204,10 @@ func main() { }, Flags: complete.Flags{ "--token": complete.PredictNothing, - "--password": complete.PredictNothing, "--dest": complete.PredictNothing, - "--log": complete.PredictNothing, "--help": complete.PredictNothing, + "--log": complete.PredictNothing, + "--password": complete.PredictNothing, }, }, "extensions": complete.Command{ @@ -237,8 +237,8 @@ func main() { "origin": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--reset": complete.PredictNothing, "--log": complete.PredictNothing, + "--reset": complete.PredictNothing, }, }, "paths": complete.Command{ @@ -246,8 +246,8 @@ func main() { "forget": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--all": complete.PredictNothing, "--log": complete.PredictNothing, + "--all": complete.PredictNothing, }, }, "add": complete.Command{ @@ -279,8 +279,8 @@ func main() { "forget": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--all": complete.PredictNothing, "--log": complete.PredictNothing, + "--all": complete.PredictNothing, }, }, "add": complete.Command{ @@ -298,9 +298,9 @@ func main() { "update": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--password": complete.PredictNothing, "--token": complete.PredictNothing, "--log": complete.PredictNothing, + "--password": complete.PredictNothing, }, }, }, @@ -366,15 +366,15 @@ func main() { "fileinfo": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--csv": complete.PredictNothing, - "--rte": complete.PredictNothing, "--rft": complete.PredictNothing, + "--rte": complete.PredictNothing, + "--csv": complete.PredictNothing, }, }, }, Flags: complete.Flags{ - "--help": complete.PredictNothing, "--installed": complete.PredictNothing, + "--help": complete.PredictNothing, "--supported": complete.PredictNothing, }, }, @@ -387,11 +387,11 @@ func main() { }, Flags: complete.Flags{ "--models": complete.PredictNothing, - "--allowdialogs": complete.PredictNothing, - "--import": complete.PredictNothing, "--revit": complete.PredictNothing, + "--import": complete.PredictNothing, "--help": complete.PredictNothing, "--purge": complete.PredictNothing, + "--allowdialogs": complete.PredictNothing, }, }, "caches": complete.Command{ @@ -415,8 +415,8 @@ func main() { "config": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--help": complete.PredictNothing, "--from": complete.PredictNothing, + "--help": complete.PredictNothing, }, }, "configs": complete.Command{ @@ -846,18 +846,18 @@ func main() { "doctor": complete.Command{ Sub: complete.Commands{}, Flags: complete.Flags{ - "--help": complete.PredictNothing, "--list": complete.PredictNothing, + "--help": complete.PredictNothing, "--dryrun": complete.PredictNothing, }, }, }, Flags: complete.Flags{ - "--version": complete.PredictNothing, - "--verbose": complete.PredictNothing, "--debug": complete.PredictNothing, "--help": complete.PredictNothing, "--usage": complete.PredictNothing, + "--version": complete.PredictNothing, + "--verbose": complete.PredictNothing, }, } complete.New("pyrevit", pyrevit).Run() diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs index aa028fbec..21c599850 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ExtensionManagerService.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using pyRevitExtensionParser; -using static pyRevitExtensionParser.ExtensionParser; +// using static pyRevitExtensionParser.ExtensionParser; // not currently used - all calls use fully qualified ExtensionParser.* namespace pyRevitAssemblyBuilder.SessionManager { @@ -11,14 +11,36 @@ namespace pyRevitAssemblyBuilder.SessionManager /// public class ExtensionManagerService : IExtensionManagerService { + private readonly int _revitYear; + // Logger is retained for potential direct use in this class (e.g. ClearParserCaches, future methods). + // Note: ExtensionParser logger is set by ServiceFactory before this service is constructed — + // do NOT call ExtensionParser.SetLogger() here as that would reset it. + private readonly ILogger _logger; private List? _cachedExtensions; + /// + /// Initialises the service with the running Revit version year for version-compatibility filtering. + /// + /// + /// The four-digit Revit release year (e.g. 2024). Pass 0 to disable version filtering. + /// + /// The logger instance. + public ExtensionManagerService(int revitYear = 0, ILogger? logger = null) + { + _revitYear = revitYear; + _logger = logger ?? new LoggingHelper(null); + } + /// /// Gets all parsed extensions (cached). /// private List GetAllExtensionsCached() { - return _cachedExtensions ??= ExtensionParser.ParseInstalledExtensions().ToList(); + if (_cachedExtensions != null) + return _cachedExtensions; + + _cachedExtensions = ExtensionParser.ParseInstalledExtensions(_revitYear).ToList(); + return _cachedExtensions; } /// @@ -28,7 +50,7 @@ public void ClearCache() { _cachedExtensions = null; } - + /// /// Clears all parser caches including the static caches in ExtensionParser. /// This ensures newly installed or enabled extensions are discovered on reload. @@ -56,7 +78,7 @@ public IEnumerable GetInstalledExtensions() public IEnumerable GetInstalledUIExtensions() { return GetAllExtensionsCached() - .Where(ext => ext.Config?.Disabled != true && + .Where(ext => ext.Config?.Disabled != true && ext.Directory.EndsWith(ExtensionConstants.UI_EXTENSION_SUFFIX, System.StringComparison.OrdinalIgnoreCase)); } @@ -67,8 +89,8 @@ public IEnumerable GetInstalledUIExtensions() public IEnumerable GetInstalledLibraryExtensions() { return GetAllExtensionsCached() - .Where(ext => ext.Config?.Disabled != true && + .Where(ext => ext.Config?.Disabled != true && ext.Directory.EndsWith(ExtensionConstants.LIBRARY_EXTENSION_SUFFIX, System.StringComparison.OrdinalIgnoreCase)); } } -} +} \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ServiceFactory.cs b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ServiceFactory.cs index bea46bdd1..0aac9f7e7 100644 --- a/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ServiceFactory.cs +++ b/dev/pyRevitLoader/pyRevitAssemblyBuilder/SessionManager/ServiceFactory.cs @@ -43,10 +43,19 @@ public static IAssemblyBuilderService CreateAssemblyBuilderService(string revitV /// /// Creates an ExtensionManagerService instance. /// + /// The Revit version number (e.g., "2024"). Used to filter extensions by version compatibility. /// A new IExtensionManagerService instance. - public static IExtensionManagerService CreateExtensionManagerService() + public static IExtensionManagerService CreateExtensionManagerService(string revitVersion, ILogger logger) { - return new ExtensionManagerService(); + int.TryParse(revitVersion, out var revitYear); + + // log if parsing fails, but still create the service with revitYear = 0 which will be treated as "no version filter" + if (revitYear == 0) + { + logger.Warning($"Failed to parse Revit version: {revitVersion}"); + } + + return new ExtensionManagerService(revitYear, logger); } /// @@ -135,7 +144,7 @@ public static IButtonBuilderFactory CreateButtonBuilderFactory(UIApplication uiA { // Create script initializers var smartButtonScriptInitializer = new SmartButtonScriptInitializer(uiApplication, logger); - + // Create individual button builders var linkButtonBuilder = new LinkButtonBuilder(logger, buttonPostProcessor); var pushButtonBuilder = new PushButtonBuilder(logger, buttonPostProcessor, smartButtonScriptInitializer); @@ -169,7 +178,7 @@ public static IStackBuilder CreateStackBuilder(UIApplication uiApplication, ILog var linkButtonBuilder = new LinkButtonBuilder(logger, buttonPostProcessor); var pulldownButtonBuilder = new PulldownButtonBuilder(logger, buttonPostProcessor, linkButtonBuilder, smartButtonScriptInitializer); var splitButtonBuilder = new SplitButtonBuilder(logger, buttonPostProcessor, linkButtonBuilder); - + return new StackBuilder(logger, buttonPostProcessor, linkButtonBuilder, pulldownButtonBuilder, splitButtonBuilder, smartButtonScriptInitializer); } @@ -247,17 +256,17 @@ public static ISessionManagerService CreateSessionManagerService( // Create logger first - it's used by all other services var logger = CreateLogger(pythonLogger); ExtensionParser.SetLogger(new ExtensionParserLoggerAdapter(logger)); - + // Create core services var assemblyBuilder = CreateAssemblyBuilderService(revitVersion, buildStrategy, logger); - var extensionManager = CreateExtensionManagerService(); + var extensionManager = CreateExtensionManagerService(revitVersion, logger); var hookManager = CreateHookManager(logger); - + // Create icon and tooltip managers var iconManager = CreateIconManager(logger); var tooltipManager = CreateTooltipManager(logger); var buttonPostProcessor = CreateButtonPostProcessor(logger, iconManager, tooltipManager); - + // Create UI builders var panelStyleManager = CreatePanelStyleManager(logger); var tabBuilder = CreateTabBuilder(uiApplication, logger); @@ -265,7 +274,7 @@ public static ISessionManagerService CreateSessionManagerService( var buttonBuilderFactory = CreateButtonBuilderFactory(uiApplication, logger, buttonPostProcessor); var stackBuilder = CreateStackBuilder(uiApplication, logger, buttonPostProcessor); var comboBoxBuilder = CreateComboBoxBuilder(uiApplication, logger, buttonPostProcessor); - + // Create ribbon scanner for UI cleanup var ribbonScanner = CreateUIRibbonScanner(logger); @@ -289,7 +298,7 @@ public static ISessionManagerService CreateSessionManagerService( ribbonScanner, logger); } - + /// /// Creates a SessionManagerService instance with custom service implementations. /// Use this overload for testing or when custom implementations are needed. @@ -319,4 +328,4 @@ public static ISessionManagerService CreateSessionManagerService( logger); } } -} +} \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs b/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs index e3005d983..ce281424e 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParser/ExtensionParser.cs @@ -79,6 +79,58 @@ private static void LogParseException(string parsedFile, Exception ex) LogError(msg); } + /// + /// Returns true if the given revitYear falls within the declared min/max version range. + /// A null or empty constraint is treated as no restriction (open-ended). + /// A non-empty value that cannot be parsed to an integer is treated as a hard fail. + /// + /// The minimum Revit version. + /// The maximum Revit version. + /// The Revit year of the running Revit instance. + /// The component or extension name, used in log messages. + private static bool IsRevitVersionCompatible(string minRevitVersion, string maxRevitVersion, int revitYear, string name) + { + bool compatible = true; + + // Parse and validate min_revit_version + if (!string.IsNullOrEmpty(minRevitVersion)) + { + if (!int.TryParse(minRevitVersion, out var min)) + { + LogWarning($"'{name}': min_revit_version value '{minRevitVersion}' is not a valid integer - skipping."); + compatible = false; + } + else if (revitYear < min) + { + LogInfo($"'{name}': skipped - requires Revit {min} or later (running {revitYear})."); + compatible = false; + } + } + + // Parse and validate max_revit_version + if (!string.IsNullOrEmpty(maxRevitVersion)) + { + if (!int.TryParse(maxRevitVersion, out var max)) + { + LogWarning($"'{name}': max_revit_version value '{maxRevitVersion}' is not a valid integer - skipping."); + compatible = false; + } + else if (revitYear > max) + { + LogInfo($"'{name}': skipped - requires Revit {max} or earlier (running {revitYear})."); + compatible = false; + } + } + + if (revitYear <= 0) + { + LogWarning("Skipping min / max version test, since Revit version is unknown"); + return true; + } + + return compatible; + } + private static int GetExceptionInt(Exception ex, params string[] keys) { if (ex?.Data == null) @@ -106,21 +158,21 @@ private static string GetExceptionString(Exception ex, params string[] keys) return string.Empty; } - + // Cache file existence checks to avoid repeated file system calls private static Dictionary _fileExistsCache = new Dictionary(); - + // Cache directory file listings to avoid repeated Directory.GetFiles calls private static Dictionary _directoryFilesCache = new Dictionary(); - + // Cache icon parsing results per component directory private static Dictionary _iconCache = new Dictionary(); - + private static bool FileExists(string path) { if (string.IsNullOrEmpty(path)) return false; - + if (!_fileExistsCache.TryGetValue(path, out bool exists)) { exists = File.Exists(path); @@ -128,7 +180,7 @@ private static bool FileExists(string path) } return exists; } - + private static string[] GetFilesInDirectory(string directory, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) { if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) @@ -153,17 +205,17 @@ private static string[] GetFilesInDirectory(string directory, string searchPatte // Cache extension roots to avoid repeated directory traversal and config reading private static List _cachedExtensionRoots; - + /// /// Flag to track if locale has been initialized from config /// private static bool _localeInitialized = false; - + /// /// Cached locale value for cache invalidation when locale changes /// private static string _cachedLocale = null; - + /// /// Clears all static caches to force re-parsing of extensions. /// This should be called before reloading pyRevit to ensure newly installed @@ -178,11 +230,11 @@ public static void ClearAllCaches() _cachedConfig = null; _pythonScriptCache.Clear(); _localeInitialized = false; - + // Also clear the BundleParser cache BundleParser.BundleYamlParser.ClearCache(); } - + /// /// Initializes the DefaultLocale from user configuration if not already set. /// Should be called before parsing extensions to ensure locale-aware localization. @@ -192,7 +244,7 @@ private static void InitializeLocaleFromConfig() { var config = GetConfig(); var userLocale = config.UserLocale; - + // Check if locale has changed since last initialization // If locale changed, we need to invalidate all caches to force re-parsing if (_localeInitialized && userLocale != _cachedLocale) @@ -200,7 +252,7 @@ private static void InitializeLocaleFromConfig() logger.Debug("Locale changed from '{0}' to '{1}'. Clearing caches...", _cachedLocale, userLocale); ClearAllCaches(); } - + if (!string.IsNullOrEmpty(userLocale)) { DefaultLocale = userLocale; @@ -208,7 +260,7 @@ private static void InitializeLocaleFromConfig() _cachedLocale = userLocale; _localeInitialized = true; } - + private static List GetCachedExtensionRoots() { if (_cachedExtensionRoots == null) @@ -222,7 +274,7 @@ private static List GetCachedExtensionRoots() return _cachedExtensionRoots; } - public static IEnumerable ParseInstalledExtensions() + public static IEnumerable ParseInstalledExtensions(int revitYear = 0) { var extensionRoots = GetCachedExtensionRoots(); @@ -253,7 +305,9 @@ public static IEnumerable ParseInstalledExtensions() var fullPath = Path.GetFullPath(extDir); if (discoveredExtensions.Add(fullPath)) { - yield return ParseExtension(extDir); + var parsed = ParseExtension(extDir, revitYear); + if (parsed != null) + yield return parsed; } } @@ -273,7 +327,9 @@ public static IEnumerable ParseInstalledExtensions() var fullPath = Path.GetFullPath(libDir); if (discoveredExtensions.Add(fullPath)) { - yield return ParseExtension(libDir); + var parsed = ParseExtension(libDir, revitYear); + if (parsed != null) + yield return parsed; } } } @@ -283,8 +339,9 @@ public static IEnumerable ParseInstalledExtensions() /// Parses a specific extension from the given extension path /// /// The full path to the .extension or .lib directory + /// The running Revit version year (e.g. 2024). Pass 0 to skip version filtering. /// A single ParsedExtension if the path is valid and contains an extension, otherwise empty - public static IEnumerable ParseInstalledExtensions(string extensionPath) + public static IEnumerable ParseInstalledExtensions(string extensionPath, int revitYear = 0) { if (string.IsNullOrWhiteSpace(extensionPath) || !Directory.Exists(extensionPath)) yield break; @@ -294,15 +351,18 @@ public static IEnumerable ParseInstalledExtensions(string exten !extensionPath.EndsWith(".lib", StringComparison.OrdinalIgnoreCase)) yield break; - yield return ParseExtension(extensionPath); + var parsed = ParseExtension(extensionPath, revitYear); + if (parsed != null) + yield return parsed; } /// /// Parses specific extensions from the given extension paths /// /// The full paths to the .extension or .lib directories + /// The running Revit version year (e.g. 2024). Pass 0 to skip version filtering. /// ParsedExtensions for valid paths that contain extensions - public static IEnumerable ParseInstalledExtensions(IEnumerable extensionPaths) + public static IEnumerable ParseInstalledExtensions(IEnumerable extensionPaths, int revitYear = 0) { if (extensionPaths == null) yield break; @@ -317,7 +377,9 @@ public static IEnumerable ParseInstalledExtensions(IEnumerable< !extensionPath.EndsWith(".lib", StringComparison.OrdinalIgnoreCase)) continue; - yield return ParseExtension(extensionPath); + var parsed = ParseExtension(extensionPath, revitYear); + if (parsed != null) + yield return parsed; } } @@ -334,11 +396,12 @@ private static PyRevitConfig GetConfig() /// Parses a single extension from the given extension directory path /// /// The path to the .extension directory - /// A ParsedExtension object - private static ParsedExtension ParseExtension(string extDir) + /// The running Revit version year (e.g. 2024). Pass 0 to skip version filtering. + /// A ParsedExtension object, or null if the extension is incompatible with the given Revit version + private static ParsedExtension ParseExtension(string extDir, int revitYear = 0) { var extName = Path.GetFileNameWithoutExtension(extDir); - + var bundlePath = Path.Combine(extDir, "bundle.yaml"); ParsedBundle parsedBundle = null; if (FileExists(bundlePath)) @@ -353,18 +416,23 @@ private static ParsedExtension ParseExtension(string extDir) } } + // Extension-level version gate: skip the entire extension (and its directory tree) + // if it declares a version range that doesn't include the running Revit year. + if (!IsRevitVersionCompatible(parsedBundle?.MinRevitVersion, parsedBundle?.MaxRevitVersion, revitYear, extName)) + return null; + // Pass extension-level templates to child components // Include author as a template if it exists - var extensionTemplates = parsedBundle?.Templates != null + var extensionTemplates = parsedBundle?.Templates != null ? new Dictionary(parsedBundle.Templates) : new Dictionary(); - + // If extension has an author, add it as a template for children to inherit if (!string.IsNullOrEmpty(parsedBundle?.Author)) { extensionTemplates["author"] = parsedBundle.Author; } - + // Read extension.json for additional templates var extensionJsonPath = Path.Combine(extDir, "extension.json"); if (FileExists(extensionJsonPath)) @@ -401,7 +469,7 @@ private static ParsedExtension ParseExtension(string extDir) } } - var children = ParseComponents(extDir, extName, null, extensionTemplates.Count > 0 ? extensionTemplates : null); + var children = ParseComponents(extDir, extName, null, extensionTemplates.Count > 0 ? extensionTemplates : null, revitYear); // Read extension config from pyRevit config file (cached). // Config is keyed by folder name (e.g. [extension_test.extension]) so it matches install and Python. @@ -534,7 +602,7 @@ private static void ReorderByLayout(ParsedComponent component, ParsedExtension e } } } - + /// /// Applies layout directives (before, after, beforeall, afterall) to reorder components. /// Directives that reference external components (not found in children) are stored @@ -674,7 +742,7 @@ private static List GetExtensionRoots() Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "pyRevit", "Extensions"); - + if (Directory.Exists(thirdPartyExtensionsPath)) { roots.Add(thirdPartyExtensionsPath); @@ -758,7 +826,7 @@ private static string SubstituteTemplates(string input, Dictionary private static Dictionary SubstituteTemplatesInDict( - Dictionary localizedValues, + Dictionary localizedValues, Dictionary templates) { if (localizedValues == null || templates == null || templates.Count == 0) @@ -776,7 +844,8 @@ private static List ParseComponents( string baseDir, string extensionName, string parentPath = null, - Dictionary inheritedTemplates = null) + Dictionary inheritedTemplates = null, + int revitYear = 0) { var components = new List(); @@ -828,12 +897,12 @@ private static List ParseComponents( foreach (var scriptExt in scriptExtensions) { var scriptFile = $"script{scriptExt}"; - scriptPath = dirFiles.FirstOrDefault(f => + scriptPath = dirFiles.FirstOrDefault(f => f.EndsWith(scriptFile, StringComparison.OrdinalIgnoreCase)); if (scriptPath != null) break; } - + // If no script.* file found, look for any file with the target extensions // This handles cases like BIM1_ArrowHeadSwitcher_script.dyn if (scriptPath == null) @@ -842,9 +911,9 @@ private static List ParseComponents( foreach (var scriptExt in scriptExtensions) { // Look for any file ending with _script{ext} or just {ext} - scriptPath = allFiles.FirstOrDefault(f => + scriptPath = allFiles.FirstOrDefault(f => (f.EndsWith($"_script{scriptExt}", StringComparison.OrdinalIgnoreCase) || - (f.EndsWith(scriptExt, StringComparison.OrdinalIgnoreCase) && + (f.EndsWith(scriptExt, StringComparison.OrdinalIgnoreCase) && !f.EndsWith($"_config{scriptExt}", StringComparison.OrdinalIgnoreCase)))); if (scriptPath != null) break; @@ -912,13 +981,13 @@ private static List ParseComponents( LogParseException(bundleYaml, ex); } } - + // Try to get content from bundle.yaml metadata first if (tempBundle != null && !string.IsNullOrEmpty(tempBundle.Content)) { scriptPath = ResolveContentPath(dir, tempBundle.Content); } - + // If no content in metadata, use naming convention if (scriptPath == null) { @@ -949,7 +1018,7 @@ private static List ParseComponents( } } } - + // Handle alternative content (CTRL+Click) if (tempBundle != null && !string.IsNullOrEmpty(tempBundle.ContentAlt)) { @@ -980,10 +1049,10 @@ private static List ParseComponents( } } } - + // Look for on/off icons for smartbuttons and toggle buttons string onIconPath = null, onIconDarkPath = null, offIconPath = null, offIconDarkPath = null; - if (componentType == CommandComponentType.SmartButton || + if (componentType == CommandComponentType.SmartButton || componentType == CommandComponentType.PushButton) { // Parse on/off icons with theme support @@ -994,7 +1063,7 @@ private static List ParseComponents( var mediaFile = FindMediaFile(dir); var bundleFile = Path.Combine(dir, "bundle.yaml"); - + // Then parse bundle and override with bundle values if they exist ParsedBundle bundleInComponent = null; if (FileExists(bundleFile)) @@ -1037,7 +1106,7 @@ private static List ParseComponents( } // Pass merged templates to child components - var children = ParseComponents(dir, extensionName, fullPath, mergedTemplates); + var children = ParseComponents(dir, extensionName, fullPath, mergedTemplates, revitYear); // First, get values from Python script string title = null, author = null, doc = null; @@ -1047,7 +1116,7 @@ private static List ParseComponents( Dictionary scriptLocalizedTitles = null; Dictionary scriptLocalizedTooltips = null; Dictionary scriptLocalizedHelpUrls = null; - + if (scriptPath != null && scriptPath.EndsWith(".py", StringComparison.OrdinalIgnoreCase)) { var scriptConstants = ReadPythonScriptConstants(scriptPath); @@ -1087,13 +1156,13 @@ private static List ParseComponents( { bundleTooltip = bundleTooltipEnUs; } - + if (!string.IsNullOrEmpty(bundleTitle)) title = bundleTitle; - + if (!string.IsNullOrEmpty(bundleTooltip)) doc = bundleTooltip; - + if (!string.IsNullOrEmpty(bundleInComponent.Author)) author = bundleInComponent.Author; } @@ -1102,7 +1171,7 @@ private static List ParseComponents( var finalLocalizedTitles = scriptLocalizedTitles ?? new Dictionary(); var finalLocalizedTooltips = scriptLocalizedTooltips ?? new Dictionary(); var finalLocalizedHelpUrls = scriptLocalizedHelpUrls ?? new Dictionary(); - + // If bundle has localized values, they override script values if (bundleInComponent?.Titles != null) { @@ -1111,7 +1180,7 @@ private static List ParseComponents( finalLocalizedTitles[kvp.Key] = kvp.Value; } } - + if (bundleInComponent?.Tooltips != null) { foreach (var kvp in bundleInComponent.Tooltips) @@ -1119,7 +1188,7 @@ private static List ParseComponents( finalLocalizedTooltips[kvp.Key] = kvp.Value; } } - + if (bundleInComponent?.HelpUrls != null) { foreach (var kvp in bundleInComponent.HelpUrls) @@ -1134,7 +1203,7 @@ private static List ParseComponents( author = SubstituteTemplates(author, mergedTemplates); var hyperlink = SubstituteTemplates(bundleInComponent?.Hyperlink, mergedTemplates); scriptHelpUrl = SubstituteTemplates(scriptHelpUrl, mergedTemplates); - + // Apply template substitution to localized values finalLocalizedTitles = SubstituteTemplatesInDict(finalLocalizedTitles, mergedTemplates); finalLocalizedTooltips = SubstituteTemplatesInDict(finalLocalizedTooltips, mergedTemplates); @@ -1145,8 +1214,8 @@ private static List ParseComponents( // so we need to check if there's actually a context defined in the bundle string finalContext; var bundleContext = bundleInComponent?.GetFormattedContext(); - if (bundleInComponent != null && - (bundleInComponent.ContextItems?.Count > 0 || + if (bundleInComponent != null && + (bundleInComponent.ContextItems?.Count > 0 || bundleInComponent.ContextRules?.Count > 0 || !string.IsNullOrEmpty(bundleInComponent.Context))) { @@ -1165,8 +1234,8 @@ private static List ParseComponents( } // Determine final highlight: bundle takes precedence over script - string finalHighlight = !string.IsNullOrEmpty(bundleInComponent?.Highlight) - ? bundleInComponent.Highlight + string finalHighlight = !string.IsNullOrEmpty(bundleInComponent?.Highlight) + ? bundleInComponent.Highlight : scriptHighlight; // Determine final help URL: bundle helpurl takes precedence over script helpurl @@ -1188,8 +1257,8 @@ private static List ParseComponents( : scriptMaxRevitVersion; // Determine final beta status: bundle takes precedence over script - bool finalIsBeta = bundleInComponent != null && bundleInComponent.IsBeta - ? bundleInComponent.IsBeta + bool finalIsBeta = bundleInComponent != null && bundleInComponent.IsBeta + ? bundleInComponent.IsBeta : scriptIsBeta; // Determine final engine config: bundle takes precedence, but script can add flags @@ -1198,6 +1267,11 @@ private static List ParseComponents( if (scriptFullFrameEngine) finalEngine.FullFrame = true; if (scriptPersistentEngine) finalEngine.Persistent = true; + // Component-level version gate: skip this component (and its children) if it declares + // a version range that doesn't include the running Revit year. + if (!IsRevitVersionCompatible(finalMinRevitVersion, finalMaxRevitVersion, revitYear, displayName)) + continue; + components.Add(new ParsedComponent { Name = namePart, @@ -1268,7 +1342,7 @@ public static string GetComponentTitle(ParsedComponent component) { if (component == null) return string.Empty; - + // First try localized titles if (component.LocalizedTitles != null && component.LocalizedTitles.Count > 0) { @@ -1276,7 +1350,7 @@ public static string GetComponentTitle(ParsedComponent component) if (!string.IsNullOrEmpty(localizedTitle)) return localizedTitle; } - + // Fall back to pre-resolved Title or DisplayName return !string.IsNullOrEmpty(component.Title) ? component.Title : component.DisplayName; } @@ -1290,7 +1364,7 @@ public static string GetComponentTooltip(ParsedComponent component) { if (component == null) return string.Empty; - + // First try localized tooltips if (component.LocalizedTooltips != null && component.LocalizedTooltips.Count > 0) { @@ -1298,7 +1372,7 @@ public static string GetComponentTooltip(ParsedComponent component) if (!string.IsNullOrEmpty(localizedTooltip)) return localizedTooltip; } - + // Fall back to pre-resolved Tooltip return component.Tooltip ?? string.Empty; } @@ -1339,7 +1413,7 @@ private static string ResolveContentPath(string bundleDir, string contentPath) // Check if it's an absolute path if (Path.IsPathRooted(contentPath)) { - if (FileExists(contentPath) && + if (FileExists(contentPath) && contentPath.EndsWith(".rfa", StringComparison.OrdinalIgnoreCase)) { return contentPath; @@ -1350,7 +1424,7 @@ private static string ResolveContentPath(string bundleDir, string contentPath) // Treat as relative to bundle directory // Normalize the path to handle .. and . properly var resolvedPath = Path.GetFullPath(Path.Combine(bundleDir, contentPath)); - if (FileExists(resolvedPath) && + if (FileExists(resolvedPath) && resolvedPath.EndsWith(".rfa", StringComparison.OrdinalIgnoreCase)) { return resolvedPath; @@ -1391,7 +1465,7 @@ private struct PythonScriptConstants } // Cache Python script constant parsing to avoid re-reading files - private static Dictionary _pythonScriptCache = + private static Dictionary _pythonScriptCache = new Dictionary(); private static PythonScriptConstants ReadPythonScriptConstants(string scriptPath) @@ -1399,7 +1473,7 @@ private static PythonScriptConstants ReadPythonScriptConstants(string scriptPath // Check cache first if (_pythonScriptCache.TryGetValue(scriptPath, out var cached)) return cached; - + var result = new PythonScriptConstants(); try @@ -1407,11 +1481,11 @@ private static PythonScriptConstants ReadPythonScriptConstants(string scriptPath // Read all lines to handle multiline strings properly var allLines = File.ReadAllLines(scriptPath); var lineIndex = 0; - + foreach (var line in allLines) { var trimmedLine = line.TrimStart(); - + if (trimmedLine.StartsWith("__title__")) { // Check if it's a dictionary @@ -1535,7 +1609,7 @@ private static PythonScriptConstants ReadPythonScriptConstants(string scriptPath { result.PersistentEngine = ExtractPythonBoolValue(trimmedLine); } - + lineIndex++; } } @@ -1563,16 +1637,16 @@ private static List ExtractPythonList(string line) var items = new List(); // Remove outer brackets value = value.Substring(1, value.Length - 2); - + // Split by comma, handling quoted strings var currentItem = ""; var inQuote = false; var quoteChar = '\0'; - + for (int i = 0; i < value.Length; i++) { var ch = value[i]; - + if (!inQuote && (ch == '"' || ch == '\'')) { inQuote = true; @@ -1595,12 +1669,12 @@ private static List ExtractPythonList(string line) currentItem += ch; } } - + // Add last item var lastTrimmed = currentItem.Trim().Trim('\'', '"'); if (!string.IsNullOrWhiteSpace(lastTrimmed)) items.Add(lastTrimmed); - + return items.Count > 0 ? items : null; } } @@ -1630,10 +1704,10 @@ private static string ExtractPythonMultilineString(string firstLine, IEnumerable int firstQuotePos = firstLineTrimmed.IndexOf("\"\"\""); if (firstQuotePos == -1) return null; - + int contentStart = firstQuotePos + 3; string partialContent = firstLineTrimmed.Substring(contentStart); - + // Check if the closing quote is on the same line int closingQuotePos = partialContent.IndexOf("\"\"\""); if (closingQuotePos != -1) @@ -1641,17 +1715,17 @@ private static string ExtractPythonMultilineString(string firstLine, IEnumerable // Single-line multiline string return partialContent.Substring(0, closingQuotePos); } - + // Need to read more lines to find the closing triple quote var content = new StringBuilder(); content.Append(partialContent); content.Append("\n"); - + foreach (var line in remainingLines) { content.Append(line); content.Append("\n"); - + // Check if this line contains the closing triple quote if (line.Contains("\"\"\"")) { @@ -1668,7 +1742,7 @@ private static string ExtractPythonMultilineString(string firstLine, IEnumerable break; } } - + // Process escape sequences in the collected content return ProcessPythonEscapeSequences(content.ToString()); } @@ -1695,18 +1769,18 @@ private static string ExtractPythonValue(string line) if (parts.Length == 2) { var value = parts[1].Trim(); - + // Try to extract quoted string first var quotedValue = ExtractPythonStringContent(value); if (quotedValue != null) return ProcessPythonEscapeSequences(quotedValue); - + // If no quotes, return the value as-is (for unquoted numbers, etc.) // Remove any trailing comments var commentIndex = value.IndexOf('#'); if (commentIndex >= 0) value = value.Substring(0, commentIndex).Trim(); - + return string.IsNullOrEmpty(value) ? null : value; } return null; @@ -1739,11 +1813,11 @@ private static string ExtractPythonStringContent(string value) return null; var trimmedValue = value.TrimStart(); - + // Find the first quote (either single or double) int startIndex = -1; char quoteChar = '\0'; - + for (int i = 0; i < trimmedValue.Length; i++) { if (trimmedValue[i] == '"' || trimmedValue[i] == '\'') @@ -1767,13 +1841,13 @@ private static string ExtractPythonStringContent(string value) endIndex += 2; continue; } - + if (trimmedValue[endIndex] == quoteChar) { // Found the closing quote return trimmedValue.Substring(startIndex + 1, endIndex - startIndex - 1); } - + endIndex++; } @@ -1845,17 +1919,17 @@ private static Dictionary ExtractPythonDictionary(string line) var dict = new Dictionary(); // Remove outer braces value = value.Substring(1, value.Length - 2); - + // Split by comma, but handle commas within quoted strings var items = new List(); var currentItem = ""; var inQuote = false; var quoteChar = '\0'; - + for (int i = 0; i < value.Length; i++) { var ch = value[i]; - + if (!inQuote && (ch == '"' || ch == '\'')) { inQuote = true; @@ -1879,10 +1953,10 @@ private static Dictionary ExtractPythonDictionary(string line) currentItem += ch; } } - + if (!string.IsNullOrWhiteSpace(currentItem)) items.Add(currentItem.Trim()); - + // Parse each key-value pair foreach (var item in items) { @@ -1895,7 +1969,7 @@ private static Dictionary ExtractPythonDictionary(string line) dict[key] = val; } } - + return dict.Count > 0 ? dict : null; } } @@ -1912,7 +1986,7 @@ private static ComponentIconCollection ParseIconsForComponent(string componentDi // Check cache first if (_iconCache.TryGetValue(componentDirectory, out var cached)) return cached; - + var icons = new ComponentIconCollection(); if (!Directory.Exists(componentDirectory)) @@ -1953,7 +2027,7 @@ private static ComponentIconCollection ParseIconsForComponent(string componentDi // Cache the result _iconCache[componentDirectory] = icons; - + return icons; } @@ -2055,11 +2129,11 @@ private static (string onIconPath, string onIconDarkPath, string offIconPath, st try { var files = GetFilesInDirectory(componentDirectory, "*", SearchOption.TopDirectoryOnly); - + foreach (var file in files) { var fileName = Path.GetFileName(file).ToLowerInvariant(); - + // Check for on icons if (fileName == "on.png" || fileName == "on.ico") onIconPath = file; @@ -2094,12 +2168,12 @@ private static string FindMediaFile(string componentDirectory) try { var files = GetFilesInDirectory(componentDirectory, "*", SearchOption.TopDirectoryOnly); - + foreach (var file in files) { var fileName = Path.GetFileName(file).ToLowerInvariant(); var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName); - + // Match by name 'tooltip' (like Python's finder='name' mode) // Supports: tooltip.mp4, tooltip.swf, tooltip.png if (fileNameWithoutExt == "tooltip") @@ -2189,4 +2263,4 @@ public static string ToExtension(this CommandComponentType type) } } } -} +} \ No newline at end of file diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/DynamoScriptTests.cs b/dev/pyRevitLoader/pyRevitExtensionParserTester/DynamoScriptTests.cs index 897f9aa7d..c04e4d988 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParserTester/DynamoScriptTests.cs +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/DynamoScriptTests.cs @@ -67,21 +67,15 @@ public void Parser_Should_Find_TestDynamoBIMGUI_With_Custom_Named_Script() var extension = parsedExtensions.First(); - // Look for Test DynamoBIM GUI button (has BIM1_ArrowHeadSwitcher_script.dyn) + // Look for Test DynamoBIM GUI button (has custom-named folie_architecturale_script.dyn) var dynamoGuiButton = FindComponentRecursively(extension, "TestDynamoBIMGUI"); - + Assert.That(dynamoGuiButton, Is.Not.Null, "Should find TestDynamoBIMGUI button"); - Assert.That(dynamoGuiButton.ScriptPath, Does.EndWith("BIM1_ArrowHeadSwitcher_script.dyn"), - "TestDynamoBIMGUI should have BIM1_ArrowHeadSwitcher_script.dyn"); - Assert.That(File.Exists(dynamoGuiButton.ScriptPath), Is.True, - "BIM1_ArrowHeadSwitcher_script.dyn file should exist"); - - // Also verify the config file exists - var configPath = Path.Combine(Path.GetDirectoryName(dynamoGuiButton.ScriptPath)!, - "BIM1_DeleteUnusedViewTemplates_config.dyn"); - Assert.That(File.Exists(configPath), Is.True, - "BIM1_DeleteUnusedViewTemplates_config.dyn should also exist"); - + Assert.That(dynamoGuiButton.ScriptPath, Does.EndWith("folie_architecturale_script.dyn"), + "TestDynamoBIMGUI should have custom-named folie_architecturale_script.dyn"); + Assert.That(File.Exists(dynamoGuiButton.ScriptPath), Is.True, + "folie_architecturale_script.dyn file should exist"); + TestContext.WriteLine($"Found TestDynamoBIMGUI script at: {dynamoGuiButton.ScriptPath}"); } diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/VersionFilteringTests.cs b/dev/pyRevitLoader/pyRevitExtensionParserTester/VersionFilteringTests.cs new file mode 100644 index 000000000..0d9f0c8c5 --- /dev/null +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/VersionFilteringTests.cs @@ -0,0 +1,156 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using pyRevitExtensionParser; +using pyRevitExtensionParserTest.TestHelpers; +using static pyRevitExtensionParser.ExtensionParser; + +namespace pyRevitExtensionParserTest +{ + /// + /// Tests for min_revit_version / max_revit_version filtering in ExtensionParser. + /// All tests use temporary on-disk extension structures and call ParseInstalledExtensions + /// directly so filtering behaviour is verified at the parser level, independently of + /// ExtensionManagerService or any real installed extensions. + /// + [TestFixture] + public class VersionFilteringTests : TempFileTestBase + { + // ------------------------------------------------------------------------- + // Test 1 — Extension excluded by min_revit_version + // ------------------------------------------------------------------------- + + [Test] + public void Extension_WithMinVersion_IsExcluded_WhenRevitYearIsTooLow() + { + // Arrange — extension requires Revit 2025, running 2024 + var builder = new TestExtensionBuilder(TestTempDir, "MinVersionExtension"); + builder.Create(); + TestExtensionBuilder.WriteBundleYaml(builder.ExtensionPath, "min_revit_version: \"2025\""); + builder.AddTab("Tab").AddPanel("Panel").AddPushButton("Button", "pass"); + + // Act + var results = ParseInstalledExtensions(new[] { builder.ExtensionPath }, revitYear: 2024).ToList(); + + // Assert + Assert.IsEmpty(results, "Extension should be excluded when running Revit version is below min_revit_version"); + } + + // ------------------------------------------------------------------------- + // Test 2 — Extension excluded by max_revit_version + // ------------------------------------------------------------------------- + + [Test] + public void Extension_WithMaxVersion_IsExcluded_WhenRevitYearIsTooHigh() + { + // Arrange — extension supports up to Revit 2022, running 2024 + var builder = new TestExtensionBuilder(TestTempDir, "MaxVersionExtension"); + builder.Create(); + TestExtensionBuilder.WriteBundleYaml(builder.ExtensionPath, "max_revit_version: \"2022\""); + builder.AddTab("Tab").AddPanel("Panel").AddPushButton("Button", "pass"); + + // Act + var results = ParseInstalledExtensions(new[] { builder.ExtensionPath }, revitYear: 2024).ToList(); + + // Assert + Assert.IsEmpty(results, "Extension should be excluded when running Revit version is above max_revit_version"); + } + + // ------------------------------------------------------------------------- + // Test 3 — Incompatible button excluded while compatible sibling survives + // ------------------------------------------------------------------------- + + [Test] + public void Button_WithMinVersion_IsExcluded_WhileCompatibleSiblingSurvives() + { + // Arrange — two buttons in the same panel; one requires Revit 2025, one has no constraint + var builder = new TestExtensionBuilder(TestTempDir, "MixedButtonExtension"); + builder.Create(); + var panel = builder.AddTab("Tab").AddPanel("Panel"); + + // Button that should be filtered out + panel.AddPushButton("FutureButton", "pass", "min_revit_version: \"2025\""); + + // Button that should survive + panel.AddPushButton("CompatibleButton", "pass"); + + // Act + var extension = ParseInstalledExtensions(new[] { builder.ExtensionPath }, revitYear: 2024) + .Single(); + var allButtons = GetAllButtons(extension); + + // Assert + Assert.IsFalse(allButtons.Any(b => b.Name == "FutureButton"), + "Button with min_revit_version: 2025 should be excluded when running Revit 2024"); + Assert.IsTrue(allButtons.Any(b => b.Name == "CompatibleButton"), + "Button with no version constraint should survive"); + } + + // ------------------------------------------------------------------------- + // Test 4 — Invalid version string is handled gracefully (no exception) + // ------------------------------------------------------------------------- + + [Test] + public void Extension_WithInvalidVersionString_IsExcluded_WithoutThrowing() + { + // Arrange — min_revit_version contains a non-integer value + var builder = new TestExtensionBuilder(TestTempDir, "InvalidVersionExtension"); + builder.Create(); + TestExtensionBuilder.WriteBundleYaml(builder.ExtensionPath, "min_revit_version: \"not_a_year\""); + builder.AddTab("Tab").AddPanel("Panel").AddPushButton("Button", "pass"); + + // Act & Assert — should not throw, and the extension should be excluded + List results = null; + Assert.DoesNotThrow(() => + { + results = ParseInstalledExtensions(new[] { builder.ExtensionPath }, revitYear: 2024).ToList(); + }, "Parsing an invalid version string should not throw an exception"); + + Assert.IsEmpty(results, "Extension with an unparseable version string should be excluded"); + } + + // ------------------------------------------------------------------------- + // Test 5 — revitYear = 0 bypasses all filtering + // ------------------------------------------------------------------------- + + [Test] + public void Extension_WithAnyVersionConstraint_IsIncluded_WhenRevitYearIsZero() + { + // Arrange — extension nominally requires a future Revit version + var builder = new TestExtensionBuilder(TestTempDir, "FarFutureExtension"); + builder.Create(); + TestExtensionBuilder.WriteBundleYaml(builder.ExtensionPath, "min_revit_version: \"2099\""); + builder.AddTab("Tab").AddPanel("Panel").AddPushButton("Button", "pass"); + + // Act — revitYear: 0 means Revit version is unknown; filtering should be skipped + var results = ParseInstalledExtensions(new[] { builder.ExtensionPath }, revitYear: 0).ToList(); + + // Assert + Assert.IsNotEmpty(results, "Extension should be included when revitYear is 0 (version unknown, filtering disabled)"); + } + + // ------------------------------------------------------------------------- + // Helper + // ------------------------------------------------------------------------- + + /// + /// Recursively collects all pushbutton-level components from an extension. + /// + private static List GetAllButtons(ParsedComponent root) + { + var result = new List(); + if (root.Children == null) return result; + + foreach (var child in root.Children) + { + if (child.Type == CommandComponentType.PushButton) + result.Add(child); + else + result.AddRange(GetAllButtons(child)); + } + + return result; + } + } +} diff --git a/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj b/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj index 49b3e1576..a82cf626d 100644 --- a/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj +++ b/dev/pyRevitLoader/pyRevitExtensionParserTester/pyRevitExtensionParserTest.csproj @@ -60,6 +60,7 @@ + diff --git a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Bundle Tests.pulldown/Test DynamoBIM GUI.pushbutton/script.dyn b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Bundle Tests.pulldown/Test DynamoBIM GUI.pushbutton/folie_architecturale_script.dyn similarity index 99% rename from extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Bundle Tests.pulldown/Test DynamoBIM GUI.pushbutton/script.dyn rename to extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Bundle Tests.pulldown/Test DynamoBIM GUI.pushbutton/folie_architecturale_script.dyn index 9df031ce2..62e262742 100644 --- a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Bundle Tests.pulldown/Test DynamoBIM GUI.pushbutton/script.dyn +++ b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Bundle Tests.pulldown/Test DynamoBIM GUI.pushbutton/folie_architecturale_script.dyn @@ -2,7 +2,7 @@ "Uuid": "14461aef-24c5-4d59-a473-4d84cbdf4fe6", "IsCustomNode": false, "Description": "Ce graphique crée une folie architecturale en utilisant une série de lignes et leur déplacement pour positionner les éléments d'ossature et de sol dans Revit.", - "Name": "BIM1_ArrowHeadSwitcher_script", + "Name": "folie_architecturale_script", "ElementResolver": { "ResolutionMap": {} },