diff --git a/DSPythonNet3/DSPythonNet3Evaluator.cs b/DSPythonNet3/DSPythonNet3Evaluator.cs index e57f073..2171298 100644 --- a/DSPythonNet3/DSPythonNet3Evaluator.cs +++ b/DSPythonNet3/DSPythonNet3Evaluator.cs @@ -794,6 +794,75 @@ private static bool IsMarkedToSkipConversion(PyObject pyObj) public override event EvaluationFinishedEventHandler EvaluationFinished; private bool registeredUnwrapMarshaler; + private const string ConnectionNodeCompatSentinel = "__dspynet3_connnode_compat__"; + + /// + /// Compatibility shim for "node-first" static methods exposed by some Dynamo/Revit libraries. + /// In older engines these could be invoked like instance methods (e.g. node.SubNodesOfSize(2)). + /// Python.NET 3 binds static methods strictly, so we patch a small set of known APIs to keep + /// existing graphs working. + /// + private static string ConnectionNodeCompatPatchCode() + { + // Keep this snippet extremely defensive: it should be a no-op if the library isn't present. + // IMPORTANT: Many runtimes forbid setting attributes on CLR *types*. However, Python.NET + // typically allows attaching attributes to CLR *instances* via the wrapper's __dict__. + // So we patch instances coming in through IN (recursively for nested lists). + return $@" +import builtins as __builtins__ + +try: + from AdvanceSteel.ConnectionAutomation.Nodes import ConnectionNode as __ConnectionNode +except Exception: + __ConnectionNode = None + +if __ConnectionNode is not None: + if not getattr(__builtins__, '{ConnectionNodeCompatSentinel}', False): + def __dspynet3__patch_connnode_instance(__obj): + # Attach instance-callable wrappers for node-first static methods. + try: + if not isinstance(__obj, __ConnectionNode): + return + + # Only patch if missing or still the raw reflected method. + def __subnodes(__n, *args, __obj=__obj, **kwargs): + return __ConnectionNode.SubNodesOfSize(__obj, __n, *args, **kwargs) + + def __existing(*args, __obj=__obj, **kwargs): + return __ConnectionNode.ExistingConnections(__obj, *args, **kwargs) + + try: + setattr(__obj, 'SubNodesOfSize', __subnodes) + except Exception: + pass + try: + setattr(__obj, 'ExistingConnections', __existing) + except Exception: + pass + except Exception: + pass + + def __dspynet3__walk_and_patch(__x): + try: + if isinstance(__x, (list, tuple)): + for __i in __x: + __dspynet3__walk_and_patch(__i) + else: + __dspynet3__patch_connnode_instance(__x) + except Exception: + pass + + setattr(__builtins__, '__dspynet3__walk_and_patch_connnode', __dspynet3__walk_and_patch) + setattr(__builtins__, '{ConnectionNodeCompatSentinel}', True) + + # Patch current inputs for this evaluation (IN exists in the module scope). + try: + __inp = globals().get('IN', None) + __builtins__.__dspynet3__walk_and_patch_connnode(__inp) + except Exception: + pass +"; + } /// /// Called immediately before evaluation starts @@ -839,6 +908,9 @@ private void OnEvaluationBegin(PyModule scope, registeredUnwrapMarshaler = true; } + + // Apply compatibility shims (safe no-op if unavailable). + scope.Exec(ConnectionNodeCompatPatchCode()); } ///