14 oct. 2024

[Dynamo += Python] PythonNet et les Interfaces de Classe .NET




Si vous développez en Python avec PythonNet, vous avez peut-être été confronté aux problèmes de l'implémentation des Interfaces .Net, voici une solution

#DynamoBIM #Revit #PythonNet #NET #Interface  #AutodeskExpertElite #AutodeskCommunity 

if this article is not in your language, use the Google Translate widget ⬈ (bottom of page for Mobile version )


Introduction

Dans cet article, nous allons explorer comment utiliser une interface de Classe .Net avec PythonNet

  • une interface ?

En .NET, une interface est un contrat que les classes doivent respecter en implémentant ses membres (méthodes, propriétés, événements). Cela permet de créer des architectures flexibles et modifiables sans dépendre directement d'une classe spécifique

  • Différence entre IronPython et PythonNet


Critère IronPython (DLR) PythonNet (CPython + .NET)
Basé sur DLR (Dynamic Language Runtime) CPython (interpréteur Python standard)
Interopérabilité avec .NET Excel (intégration native via CLR) Très bonne (accès direct via clr en CPython)
Interopérabilité avec les objets COM Excellente (grâce à la prise en charge dynamique du DLR) Possible, mais nécessite parfois des conversions de types explicites
Gestion des objets dynamiques Très naturelle et intuitive (support direct du DLR) Plus limitée, demande souvent des déclarations explicites et des conversions de types
Utilisation des bibliothèques CPython Non (pas de support pour les modules C comme NumPy, SciPy, etc.) Oui (support complet des modules CPython comme NumPy)
Limitation par le GIL Non (pas de Global Interpreter Lock) Oui (limité par le GIL de CPython)
Multithreading Véritable exécution parallèle (pas de GIL) Concurrence limitée à cause du GIL
Interopérabilité avec les types statiquement typés Excellente (gestion par le CLR) Excellente (intégration directe des types .NET)
Interopérabilité avec les bibliothèques .NET dynamiques Très fluide grâce au DLR Moins fluide pour les objets dynamiques, meilleure pour les types statiques
Performance en multithreading Très bonne (pas de GIL) Limité par le GIL, sauf si multiprocessing est utilisé
Gestion de la mémoire Gérée par le garbage collector du CLR Gérée par le garbage collector de CPython

Explications CLR / DLR:

    • CLR est le runtime de base utilisé pour tous les langages .NET, gérant principalement des langages statiquement typés et offrant des services comme la gestion de la mémoire, la gestion des exceptions, et la sécurité.

    • DLR est une couche supplémentaire au-dessus du CLR, conçue spécifiquement pour prendre en charge les langages dynamiques et permettre des opérations dynamiques, comme la modification des objets et l'exécution de méthodes à l'exécution.




Création d'un Objet .Net  

(avec héritage d'une Interface)

       
    • Avec  IronPython, une instance d'une classe qui hérite d'un type .NET est un objet .NET. Cela signifie que bien que l'objet soit créé à partir d'une classe définie dans IronPython, il est traité comme un objet .NET dans le runtime .NET (CLR)


  class Temperature(System.IComparable):
    def __init__(self, init_value):
        self.__t = System.Int32(init_value)


    def CompareTo(self, obj):
        if obj is None:
            return System.Int32(1)
        else:
            return self.Celsius.CompareTo(obj.Celsius)

    @property
    def Celsius(self):
        return self.__t

    @Celsius.setter
    def Celsius(self, value: System.Int32):
        self.__t = System.Int32(value)

    • Avec PythonNet, il faut rajouter une syntaxe spécifique 

- Un espace de nom spécifique avec l'attribut __namespace__

- Lorsque vous héritez de types .NET en Python, si vous utilisez/surchargez la méthode d'initialisation  __init__, vous devez appeler explicitement le constructeur de base en utilisant super().__init__(.....) (surtout valable avec PythonNet3).



class Temperature(System.IComparable):
    __namespace__ = "My.Temperature"
    def __init__(self, init_value):
        self.__t = System.Int32(init_value)
        super().__init__()

    def CompareTo(self, obj):
        if obj is None:
            return System.Int32(1)
        else:
            return self.Celsius.CompareTo(obj.Celsius)

    @property
    def Celsius(self):
        return self.__t

    @Celsius.setter
    def Celsius(self, value: System.Int32):
        self.__t = System.Int32(value)



Le problème, c'est qu'à chaque appel de cette classe est créé :
    • 1 Espace de Nom  + 1 Type 
        • avec PythonNet 2.5 → localisable dans l'Assembly Python.Runtime 
        • avec PythonNet 3 → localisable dans sys.modules 

Il en résulte que l'exécution d'un script python avec des types contenant __namespace__, plus d'une fois, lèvera une exception Duplicate Type Name
.




Solution "uuid" : un espace de nom différent a chaque appel



import clr
import sys
import System
#import net library
from System import Array, GC
from System.Collections.Generic import List, IList, Dictionary
#import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB

#import Revit APIUI namespace
clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Selection import *

my_path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments)

import uuid
import os
import logging
from logging.handlers import RotatingFileHandler
import traceback

## Start create logger Object ##
logger = logging.getLogger("TestInterface_ByRename_Logger")
# set to  DEBUG
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(funcName)s :: %(process)d :: %(message)s')
# create handler 
file_handler = RotatingFileHandler(my_path + '\\TestInterface_ByRename_Logger.log', mode='a', maxBytes=100000, backupCount=1)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.disabled = False


class Custom_SelectionElem1(ISelectionFilter):
    __namespace__ = "CustomSelectionElem_"+ str(uuid.uuid4())
    def __init__(self, bic):
        super().__init__()
        self.bic = bic
    def AllowElement(self, e):
        if e.Category.Id == ElementId(self.bic):
            return True
        else:
            return False
    def AllowReference(self, ref, point):
        return True 
        
        
def get_Memory(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        GC.Collect()
        memory = GC.GetTotalMemory(True) / 1024
        type_name = ""
        total_number_type = 0
        for asm in System.AppDomain.CurrentDomain.GetAssemblies():
            for atyp in asm.GetTypes():
                if "Custom_SelectionElem" in atyp.Name :
                    total_number_type += 1
                    type_name = atyp.Name
        for module_name in dict(sys.modules).keys():
            if "Custom_SelectionElem" in module_name :
                total_number_type += 1
                type_name = module_name
        logger.info((func.__name__, type_name, memory, total_number_type))
        return result
    return wrapper
        
@get_Memory
def Test_Rename_NameSpace():
    a = Custom_SelectionElem1(BuiltInCategory.OST_AbutmentPiles)
        
        
Test_Rename_NameSpace()

for handler in logger.handlers:
    if isinstance(handler, logging.FileHandler):
        handler.close()

Ici la solution consiste à générer un nouvel __namespace__ à chaque appel de la classe, avec la librairie uuid (UUID aléatoire a chaque appel).

Cependant, lorsque nous lançons plusieurs fois ce script, nous observons qu'à chaque lancement :
    • le runtime PythonNet crée un nouveau Type en mémoire
    • la mémoire utilisée augmente en conséquence (même en fermant Dynamo entre chaque lancement)
      (prenant soin de supprimer les objets non référencés avec GC.Collect() )



lancement successif du script Dynamo - Solution "uuid"

Solution "__import__()" : re-utiliser le Type généré

Exemple :

import clr
import System
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *

clr.AddReference('RevitAPIUI')
import Autodesk.Revit.UI as RUI
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Selection import *

clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument


class Custom_SelectionElem:
    def __new__(cls, *args, **kwargs):
        cls.args = args
        # name of namespace : "CustomNameSpace" + "_" + shortUUID (8 characters)
        # IMPORTANT NOTE Each time you modify this class, you must change the namesapce name.
        cls.__namespace__ = "SelectionNameSpace_tEfYX0DHE"
        try:
            # 1. Try to import the module and class. If it already exists, you've probably already created 
            module_type = __import__(cls.__namespace__)
            return module_type._InnerClassInterface(*cls.args)
        except ImportError as ex:
            print(ex)
            # 2. If we get an ImportError, then the derivative class hasn't been built. Build it. Make sure to set the
            #    __namespace__ so that it is created with the appropriate .Net namespace.
            class _InnerClassInterface(ISelectionFilter):
                __namespace__ = cls.__namespace__
                #
                def __init__(self, bic):
                    super().__init__()
                    self.bic = bic
                    
                def AllowElement(self, e):
                    if e.Category.Id == ElementId(self.bic):
                        return True
                    else:
                        return False
                def AllowReference(self, ref, point):
                    return True 
                    
            return _InnerClassInterface(*cls.args)
       
for i in range(3):
    RUI.TaskDialog.Show("Select", f"Select a door {i+1}/3")
    ref = uidoc.Selection.PickObject(ObjectType.Element, Custom_SelectionElem(BuiltInCategory.OST_Doors))
    print(ref.ElementId)
 
Cette fois, la solution est de créer un Type de Classe (avec Namespace) une seule fois et de réutilisé celui lors des appels suivants.

Pour cela, j'utilise ici :
    • la méthode __new__() afin de pouvoir construire et retourner un objet
    • la méthode __import__() pour essayer d'importer le namespace (module) pendant l'exécution
ainsi, nous réduisons le coût en mémoire par rapport la méthode précédente

lancement successif du script Dynamo - Solution "__import__()"

Note pour le nommage du cls.__namespace__ :
    • je recommande un nom unique avec éventuellement un short UUID
    • à chaque fois que vous modifierez cette classe, il faudra changer cette variable (d'où le short UUID ) sinon vous allez charge les anciens types/modules (jusqu'au redémarrage de l'application hôte)
Compatibilité de cette méthode :
 
    • Revit / Civil 2022 - CPython3/PythonNet2.5 : Oui
    • Revit / Civil 2023 - CPython3/PythonNet2.5 : Oui
    • Revit / Civil 2024 - CPython3/PythonNet2.5 : NON
    • Revit / Civil 2025 - CPython3/PythonNet2.5 : Oui
    • Revit / Civil 2025+ - PythonNet3 : Oui

Cas des Interfaces de Classe .Net avec des paramètres out


Les méthodes .NET qui ont des paramètres out  au sein des Interfaces de classe .Net, leurs valeurs doivent être retournées dans un tuple comprenant :
    • la valeur de retour de la méthode
    • la valeur de retour du / des valeur(s) modifiée(s) des paramètres out
Attention cette syntaxe ne fonctionne qu'avec PythonNet3, n'essayez pas avec CPython3/PythonNet2 sous peine de faire crasher l'application hôte.

Exemple :


import clr
import System
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *

clr.AddReference('RevitAPIUI')
import Autodesk.Revit.UI as RUI
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Selection import *

clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument

#         
class Custom_FamilyOption:
    def __new__(cls, *args, **kwargs):
        cls.args = args
        # name of namespace : "CustomNameSpace" + "_" + shortUUID (8 characters)
        # IMPORTANT NOTE Each time you modify this class, you must change the namesapce name.
        cls.__namespace__ = "FamilyOptionNameSpace_tEfYX0DHE"
        try:
            # 1. Try to import the module and class. If it already exists, you've probably already created it once, so you can use it.
            module_type = __import__(cls.__namespace__)
            return module_type._InnerClassInterface(*cls.args)
        except ImportError as ex:
            print(ex)
            # 2. If we get an ImportError, then the derivative class hasn't been built. Build it. Make sure to set the
            #    __namespace__ so that it is created with the appropriate .Net namespace.
            class _InnerClassInterface(IFamilyLoadOptions) :
                __namespace__ = cls.__namespace__
            
                def __init__(self):
                    super().__init__() 
                    
                def OnFamilyFound(self, familyInUse, _overwriteParameterValues):
                    overwriteParameterValues = True
                    return (True, overwriteParameterValues)
            
                def OnSharedFamilyFound(self, sharedFamily, familyInUse, source, _overwriteParameterValues):
                    overwriteParameterValues = True      
                    return (True, overwriteParameterValues)
                    
            return _InnerClassInterface(*cls.args)
#        
def loadFamily(path):
    TransactionManager.Instance.EnsureInTransaction(doc)
    opts = Custom_FamilyOption()
    dummyFamily = None
    loadFamily = doc.LoadFamily(path, opts, dummyFamily)
    TransactionManager.Instance.TransactionTaskDone()

for path in IN[0] : loadFamily(path)



À mesure que PythonNet continue de s'améliorer, notamment avec la version 3, nous pouvons nous espérer à une simplification de ces processus à l'avenir.




"La simplicité est la sophistication ultime."
— Steve Jobs

28 sept. 2024

[Hors Sujet BIM] Nouvelle NFC15-100


 




L’AFNOR a publié le 23 août une nouvelle version de la norme NF C 15-100

La norme NFC 15-100, qui régit les installations électriques basse tension en France, a subi une révision majeure pour répondre aux évolutions technologiques et aux exigences européennes.

#NFC 15-100 #Euroclasses #AFNOR #FFIE

6 sept. 2024

[Dynamo += Python] Selection via une DataGrid (WPF)





Un exemple de sélection d'éléments au travers d'une interface de type 'DataGrid'

#DynamoBIM #Revit #Python #DataTable #WPF #AutodeskExpertElite #AutodeskCommunity 

9 août 2024

[Dynamo += Python] HelixToolKit




Découverte de la librairie HelixToolKit avec Dynamo

#DynamoBIM #Revit #Python  #HelixToolkit #WPF #AutodeskExpertElite #AutodeskCommunity 

7 juil. 2024

[Dynamo += Python] Introduction Autodesk Plateforme Services (Partie 2/2)





Suite d'une introduction a l'API Autodesk Plateforme Service directement (APS) depuis Dynamo sur la plateforme ACC

#DynamoBIM #Revit #Python  #APS #ACC        #AutodeskExpertElite #AutodeskCommunity 

23 juin 2024

[Dynamo += Python] Introduction Autodesk Plateforme Services (Partie 1/2)

  




Petite introduction a l'API Autodesk Plateforme Service directement (APS) depuis Dynamo (sur la plateforme ACC)

#DynamoBIM #Revit #Python  #ACC #APS    #AutodeskExpertElite #AutodeskCommunity 

25 mai 2024

[Dynamo += Python] Trouver la courbe médiane entre 2 courbes





Comment trouver la courbe moyenne entre deux courbes, qui ne sont pas forcément parallèles. 

#DynamoBIM #Revit #Python  #Geometry #AutodeskExpertElite #AutodeskCommunity 

5 mai 2024

[Dynamo += Python] Les joies du mode Périodique (épisode 2)

 




 Vous aimez les transformations et les géométries en mouvement… vous êtes bien tombé

#DynamoBIM #Revit #Python #PerodicMode #Fractale #Tesserac #Geometry #AutodeskExpertElite #AutodeskCommunity 

9 avr. 2024

[Dynamo += Python] Export IFC : configurations Utilisateur







Une astuce pour obtenir les données des configurations d'Export IFC


#DynamoBIM #Revit #Python #RevitAPI #IFC #ConfigurationIFC #AutodeskExpertElite #AutodeskCommunity 

17 mars 2024

[Dynamo += Python] Organisation de l'Arborescence de Projet (Vues)

 






Comment retrouver l'Arborescence des Vues d'un Projet Revit ?


#DynamoBIM #Revit #Python #RevitAPI #BrowserOrganization #AutodeskExpertElite #AutodeskCommunity 

3 mars 2024

[Dynamo += Python] Comparaison de Solides via Sérialisation

 






Un exemple de sérialisation pour comparer des éléments par topologie (solides)

#DynamoBIM #Revit #Python #Solid #Serialization #AutodeskExpertElite #AutodeskCommunity 

28 janv. 2024

[Dynamo += Python] fonctions Python VS fonctions .Net

 





Lors de la manipulation d'objets .NET, il est parfois nécessaire de choisir entre les fonctions BuiltIn Python et les méthodes d'extension LINQ. Cependant, la meilleure approche ne dépend pas toujours de la méthode à laquelle on pense initialement, surtout avec l'évolution récente de .NET.


#DynamoBIM #Revit  #Net #Python #IronPython #LINQ

30 déc. 2023

[Dynamo += Python] Import d'Annotations Dwfx (Feuilles)

 








Importer des annotations Dwfx via l'API Revit

#DynamoBIM #Revit  #Dwfx#Python #DesignReview

9 déc. 2023

[Revit Tips] Copier Coller du contenu Excel/Word







Copier-Coller du contenu Excel ou Word dans une annotation Revit

#Excel #Revit  #Copy #Word

26 nov. 2023

[Dynamo += Python] Point dans un Polygone ?

  •  





Comment vérifier si un point se trouve à l'intérieur d'un polygone fermé ?

#Polygon#RevitAPI #DynamoBIM #IsInside