CPython3 error when calling Dynamo Node

When reusing an IronPython2 script as a CPython3 script, there seem to be an error while calling on Dynamo Nodes inside the script. It works fine in IronPython2. But does not work anymore in CPython.

Any suggestions to make this work again?


ERROR_CPYTHON.dyn (19.2 KB)

Can you stay in IronPython?

I’ve had no end of issues trying to ‘upgrade’.

What do you get when you append another line of OUT = lstOUT, DN.PolySurface.ByJoinedSurfaces.__doc__ to then end of the code string?

My guess is the line DN = Autodesk.DesignScript.Geometry is causing the issue, but I can’t confirm.

CPython3 gives me:
Autodesk.DesignScript.Geometry.PolySurface ByJoinedSurfaces(System.Collections.Generic.IEnumerable`1[Autodesk.DesignScript.Geometry.Surface])

IronPython2 gives me:
ByJoinedSurfaces(surfaces: IEnumerable[Surface]) → PolySurface

And so the question becomes is lstIn a List of surfaces, or something else?

OUT = lstIn.__class__ can help debug that.

I’am willing to try. And at 1 point in time everybody needs to upgrade. But … no Revit 2025 at the moment.

I started with some heavy theorethical testing and reading.

And I also used and modified jacob.smalls bulk upgrade script on dynamo scripts and nodes.
E.g. on the packages we use. (Almost no package was completly ready.)

And I also really want to upgrade my own package. It can be a benchmark for me if it works or fails.

lstIN is the result of UnwrapElement(IN[0])

CPython3 gives me:
System.Collections.Generic.List`1[System.Object]

IronPython2 gives me:
IronPython.Runtime.Types.PythonType

IronPython seems to wrap things differently than CPython3.
I have read something about changing Types to Classes. Not sure if that is somehow related to this.
Common migration problems — Supporting Python 3: An in-depth guide (python3porting.com)

Then the Flatten isn’t working. Try recoding that line to use a new variable so you can confirm if the action is working. Generally speaking I don’t recommend using Dynamo’s List operations in an environment other than Dynamo itself and DesignScript; when you call via Python or C# you run into issues, and those languages have more efficient means of accomplishing the same by maintaining the native method.

Thanks for the answer. I will try that tomorrow.

We need to cast to a List[Surface]

import sys
import clr
import System 
from System.Collections.Generic import List

clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import Autodesk.DesignScript.Geometry as DN

clr.AddReference('DSCoreNodes')
import DSCore
import traceback

lstIN = UnwrapElement(IN[0])
lstIN = DSCore.List.Flatten([lstIN],-1)
lstIN = List[DN.Surface]([x for x in lstIN])
lstOUT = DN.PolySurface.ByJoinedSurfaces(lstIN)

OUT = lstOUT
1 Like

Without getting TOO technical I wanted to elaborate on what this is failing, and then provide my solution (similar to @c.poupin’s but doesn’t require converting to a .NET list).

The CPython engine is an implementation of PythonNET.
IronPython engine is an implementation of IronPython (either 2 or 3 depending on which you’re using).

IronPython does the best job of converting the .NET types to Python as it is entirely focused on interop - one choses IronPython as they want to use .NET in a Python environment.
But PythonNET is intended to allow execution of Python in a .NET environment, with some degree of effort to make conversions work. This means that it generally works for an object, but collection elements won’t always work out.

In the case of List.Flatten (and likely all Dynamo list nodes) the wrapper doesn’t get inherited, so we get the base results of the node which is a list of objects, and while IronPython can use that when calling the next function (PolySurface.ByJoinedSurface) the CPYthon engine is saying “This node expects a list of surfaces, you gave me a list of objects, so that’s clearly going to fail.”

So my preferred way to scale builds is to convert any type of collection to a native Python object type, as both CPython and IronPython work just fine and you can ignore types. This can be done like so:

import clr #import the common languge runtime so we can call .NET libraries
clr.AddReference('ProtoGeometry') #add the Dynamo geometry library to the CLR
import Autodesk.DesignScript.Geometry as DG #import the Dynamo geometry class as DG so we can call it by it's alias to prevent namespace collision issues
clr.AddReference('DSCoreNodes') #add the Dynamo core nodes library to the CLR
import DSCore as DC #import the Dynamo Core class as DC so we can call it by it's alias to prevent namespace collision issues

lstIN = IN[0] #the list of objects from the Dynamo environment
flatList = [i for i in DC.List.Flatten([lstIN],-1)] #use list comprehension to take each item in the List.Flatten results and insert it in a native Python list
result = DG.PolySurface.ByJoinedSurfaces(flatList) #generate the polysurface from the flat list of objects from the Dynamo library

OUT = result #return the polysurface to the Dynamo environment
4 Likes

Thanks @jacob.small and @c.poupin both your solutions work.
I also love the methodic way in wich this problem was solved.
Calling the flatten node does not work in CPython3 as it did in IronPython2 just as Jacob said.

A 3th solution without list comprihansion does also work. Just Unwrap the flattend list.
lstIN = UnwrapElement(DSCore.List.Flatten([lstIN],-1))
Not sure what is the most flexible and most fast solution between those 3.

That brings me to the use of the Dynamo flatten node in a Python script.
I have searched for a solution before. The biggest problem was that you never know what the structure of a list can be in Dynamo. And most solutions flatten a list just 1 level deep. Or you need extra libraries. And not al of them worked in IronPython2 before. So using the flatten node did a very good job in scripts I wrote before.

Obviously I need to change my old scripts. And do something new in future scripts when i need to flatten a list.
What is a prevered pythonic way that works. Any ideas? I would love to hear them.

I have always been a fan of option 3 here, but this link covers all the methods I can think of offhand: Python | Convert a nested list into a flat list - GeeksforGeeks

1 Like

Thanks @jacob.small. Option 3 is the only one that can actually flatten an irregular list with a recursion in it.
But I remember also expamples with a yield. And expamples that prevent strings to behave as a list.
I will do some testing, and try to find the most stable and fast method.

Option 3 is asking ‘is this a list’ with the == comparison, so you should have no worries with strings - double check as always though.

Here is a python function for flattening a list if it is a list of list, with one showing how you can do a check for if it is a list of a list before flattening and the other just flattens it.

def flatten(xss):
	if any(isinstance(el, list) for el in xss):
	
		return [x for xs in xss for x in xs]
	else:
		return xss

OR

def flatten(xss):
	return [x for xs in xss for x in xs]
1 Like

I did some more testing on a solution to flatten any list in a CPython node, just as I did in IronPython nodes.
A solution that looks and works just like the native Dynamo flatten node.

  • Multiple levels is mandatory
  • As fast and reliable as possible
  • Option to flatten to a certain amound just like the node

For testing I used a list of 80.000+ items, 10 levels deep and all kinds of Python and Dynamo content.
Dynamo/Revit content might break a nice workflow. So UnwrapElement is sometimes needed.

I found and used nice solutions on stackoverflow

def _flatten(xs,lv=-1,int=1):
    def _loop(ys,lv=-1,int=1):
        for i in ys:
            if isinstance(UnwrapElement(i), list) and (int<=lv or lv<=-1): # slower but more stable
            #if isinstance(i, list) and (int<=lv or lv<=-1): #half the time but sometimes less stable result
                yield from _flatten(i,lv,int+1)
            else:
                yield i
    res = _loop(xs,lv,int)
    return res

And the option with a list instead of a yield works also just fine.

def _flatten(xs,lv=-1,int=1):
    res = []
    def _loop(ys,lv=-1,int=1):
        for i in ys:
            if isinstance(UnwrapElement(i), list) and (int<=lv or lv<=-1): # slower but more stable
            #if isinstance(i, list) and (int<=lv or lv<=-1): #half the time but sometimes less stable result
                _loop(i,lv,int+1)
            else:
                res.append(i)
    _loop(xs,lv,int)
    return res

While calling this definition the argument lv is not needed.
Just using flatten(myList) will flatten everything just like flatten(myList,-1) would have done.
The last argument int=1 is just a helper/counter, so the definition will know when to stop.

This is the Dynamo script for more testing and the 8 rough examples. And 3 examples with level option included.
Building and working with long list will take time. :wink: I also added a time.sleep to count time. So be a little patient while testing. flatten.dyn (93.0 KB)

While using TuneUp you can also conclude a few other things.

  • The flatten node wins all the times. Loading Python and all the libraries simply takes time. Even without the time.sleep(1) the native node seems to be half the excecution time. Nonetheless I’am not really unhappy, because this function seems te be almost as fast as calling the node in an IronPython script -as I used to-
  • Unwrapping a complete list in IronPython is logically much faster than checking and unwrapping parts as needed in CPython.

Only Flatten:

Flatten and UnwrapElement:

Only Flatten and without the time.sleep(1)

Hi,

this is not a solution with CPython3 engine but here is an alternative with Linq extension SelectMany with PythonNet3 and IronPython3

PythonNet3 code

import sys
import clr
import System
from System.Collections.Generic import List, IList, Dictionary
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *

clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)

import traceback

def FlattenWithLinq(*args):
    for x in args:
        if hasattr(x, "__iter__") and not isinstance(x, (str, System.String)):
            iList = List[System.Object](x)
            iList = iList.SelectMany[System.Object, System.Object](
                                                    System.Func[System.Object, List[System.Object]](
                                                            lambda p : FlattenWithLinq(p) if hasattr(p, "__iter__") \
                                                                and not isinstance(p, (str, System.String)) else [p])).ToList()
            yield from iList
        else:
            yield x

iList = IN[0]

test = list(FlattenWithLinq(iList))

OUT = test

Ironpython3 code

import sys
import clr
import System
from System.Collections.Generic import List, IList, Dictionary
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *

clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)

import traceback

def FlattenWithLinq(*args):
    for x in args:
        if hasattr(x, "__iter__") and not isinstance(x, (str, System.String)):
            iList = List[System.Object](x)
            iList = iList.SelectMany(lambda p : FlattenWithLinq(p) if hasattr(p, "__iter__") \
                            and not isinstance(x, (str, System.String)) else [p]).ToList()
            yield from iList
        else:
            yield x

iList = IN[0]
test = list(FlattenWithLinq(iList))

OUT = test