14 juil. 2025

[Dynamo += Python] Inspection des objets .Net en Markdown

 


Vous explorez vos objets .NET avec dir() avec Python ? Voici une méthode pour aller un peu plus loin !


#DynamoBIM #Python #RevitAPI   #AutodeskExpertElite #AutodeskCommunity 

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

La fonction dir() en Python affiche uniquement la liste brute des membres de l'objet. Mais elle ne vous dit rien sur leur utilité, ni sur la logique métier derrière chaque méthode ou propriété et encore moins pour les objets .Net.

Voici une alternative avec une classe Python NetType_Utils qui récupère (parse) la documentation XML de l'Assembly de l'objet à examiner.

Cette classe inspecte les objets .NET avec 2 étapes principales :

  • inspection des Types (méthodes, propriétés, etc.) avec System.Reflection

  • association de la documentation, à condition que le fichier XML soit généré lors de la compilation.







code de la classe NetType_Utils

(compatible CPython3 / IronPython3 / PythonNet3 )

# Load the Python Standard and DesignScript Libraries
import sys
import System
import clr
from System.Reflection import BindingFlags

import xml.etree.ElementTree as ET
import os
import re
import difflib
from difflib import get_close_matches

class NetType_Utils:
    @staticmethod
    def get_Type_Infos(obj):
        # sub functions
        def get_docstring(prefix, class_name, member_name=None, use_diff_lib=True):
            if member_name is None:
                doc_key = f"{prefix}:{class_name}"
            else:
                doc_key = f"{prefix}:{class_name}.{member_name}"
            docstring = doc_map.get(doc_key, None)
            if docstring is None:
                docstring = doc_map.get(doc_key.replace(" ", ""), None)
                if docstring is None and use_diff_lib:
                    filter_name_keys = [x for x in doc_map.keys() if x.startswith(f"{prefix}:{class_name}")]
                    best_matches = get_close_matches(doc_key, filter_name_keys, n=2, cutoff=0.9)
                    if best_matches:
                        docstring = doc_map.get(best_matches[0], None)
        
            return docstring if docstring is not None else "No description available."
    
        ############ MAIN ############
        try:
            cls = obj.GetType()
        except:
            try:
                cls = clr.GetClrType(obj)
            except:
                raise Exception("Error, the input object is not a Net Object")
        #
        assembly = System.Reflection.Assembly.GetAssembly(cls)
        doc_xml_path = os.path.splitext(assembly.Location)[0] + ".xml"
        doc_map = {}
        #
        try:
            if os.path.exists(doc_xml_path):
                tree = ET.parse(doc_xml_path)
                root = tree.getroot()
                for member in root.findall(".//member"):
                    member_name = member.get('name')
                    if ".#ctor" in member.get('name'):
                        member_name = member_name.replace(".#ctor", "").strip()
                        if not member_name.endswith(")"):
                            member_name += "()"
                    summary_node = member.find("summary")
                    if member_name and summary_node is not None and summary_node.text:
                        # Clean up whitespace from the docstring
                        doc_map[member_name] = ' '.join(summary_node.text.strip().split())
                        if "System." in member_name:
                            alternative_member_name = member_name.replace("System.", "")
                            doc_map[alternative_member_name] = ' '.join(summary_node.text.strip().split())
        except Exception as e:
            # If XML fails, we can't get docs, but the script can still run
            pass
       
        flags =  BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly
        
        markdown_lines = []
        # Class Name and Docstring
        markdown_lines.append(f"## {cls.FullName}")
        class_doc_key = f"T:{cls.FullName}"
        class_doc = doc_map.get(class_doc_key, "")
        if class_doc:
            markdown_lines.append(f"*{class_doc}*")
        # add inheritance
        inheritance_types = f"- Inheritance Hierarchy : {cls.FullName}"
        current = cls.BaseType
        for _ in range(10):
          if hasattr(current, "FullName") and not inheritance_types.endswith(current.FullName):
              inheritance_types += " -> " + current.FullName
              current = current.BaseType
        #
        markdown_lines.append(inheritance_types)
        markdown_lines.append("") # Add a blank line for spacing
        
        # --- Process constructors ---
        ctor_infos = cls.GetConstructors (flags)
        if ctor_infos:
            markdown_lines.append("### Constructors")
            for ctor_info in sorted(ctor_infos, key=lambda x: x.Name):
                constructor_name = ctor_info.ToString().replace("Void .ctor", cls.FullName).strip()
                docstring = get_docstring("M", constructor_name)
                markdown_lines.append(f"- `{constructor_name}` — {docstring}")
            markdown_lines.append("")
            
        # --- Process Methods ---
        methods = cls.GetMethods(flags)
        if methods:
            markdown_lines.append("### Methods")
            for method in sorted(methods, key=lambda x: x.Name):
                if not method.Name.startswith(("get_","set_", "op_")):
                    b_method_name = method.ToString()
                    return_value = "Void"
                    is_static = "static " if method.IsStatic else ""
                    check_patern = re.match(r"^(.+?)\s(.*\(.*\))$", b_method_name)
                    if check_patern is not None:
                        return_value = check_patern.group(1)
                        method_name = check_patern.group(2)
                    docstring = get_docstring("M", cls.FullName, method_name)
                    markdown_lines.append(f"- `{is_static}{method_name}` → `{return_value}` — {docstring}")
            markdown_lines.append("")
        
        # --- Process Properties ---
        properties = cls.GetProperties(flags)
        if properties:
            markdown_lines.append("### Properties")
            for prop in sorted(properties, key=lambda x: x.Name):
                docstring = get_docstring("P", cls.FullName, prop.Name)
                markdown_lines.append(f"- `{prop.Name}` — {docstring}")
            markdown_lines.append("")
            
        # --- Process Enumerations ---
        if cls.IsEnum:
            enums = cls.GetEnumNames()
            if enums:
                markdown_lines.append("### Enumerations")
                for enum in sorted(enums):
                    docstring = get_docstring("F", cls.FullName, enum)
                    markdown_lines.append(f"- `{enum}`— {docstring}")
                markdown_lines.append("")
        return "\n".join(markdown_lines)

OUT = NetType_Utils
...
Voici en vidéo, un exemple d'utilisation



Note :

Dans cet exemple, nous ne listons pas les membres des classes hérités (cela peut être fait avec la propriété BaseType).

Autre Finalité ? :

Outre de pouvoir inspecter les membres des objet.Net, nous pouvons, sur le même principe, générer un RAG de l'API Revit (avec des mots clés ) pour un LLM de façon dynamique.

Vous n'avez donc plus à vous soucier de la version de l'API, puisque vous êtes au sein même d'un contexte Revit.


Test d'un RAG sur un simple Agent IA



« L'avenir est une porte, le passé en est la clé. »
Victor Hugo

0 commentaires:

Enregistrer un commentaire