specklesystems / specklepy

Python SDK 🐍
https://speckle.systems
Apache License 2.0
108 stars 37 forks source link

Recursion exception when receiving object via transport #68

Closed tsvilans closed 3 years ago

tsvilans commented 3 years ago

Executing this:

from speckle.api.client import SpeckleClient
from speckle.api.credentials import get_default_account, get_local_accounts
from speckle.api import operations
from speckle.transports.server import ServerTransport

def main():
    account = get_default_account()

    if not account:
        raise Exception("No accounts available!")

    client = SpeckleClient(host=account.serverInfo.url, use_ssl=True)
    client.authenticate(account.token)

    streams = client.stream.list()
    stream = client.stream.get(id=streams[0].id)

    commit = stream.branches.items[0].commits.items[0]

    transport = ServerTransport(client, stream.id)
    res = operations.receive(commit.referencedObject, transport)

main()

Expected vs. Actual Behavior

Expecting to receive res object. Instead it goes into a loop:

Traceback (most recent call last):
  File "C:\git\SpeckleBlender\SpeckleBlender-2.0-test01.blend\Text.001", line 23, in <module>
  File "C:\git\SpeckleBlender\SpeckleBlender-2.0-test01.blend\Text.001", line 21, in main
  File "\\TAS-03\assets\3d modelling\Plug-ins\Blender\modules\speckle\api\operations.py", line 69, in receive
    return serializer.read_json(obj_string=obj_string)
  File "\\TAS-03\assets\3d modelling\Plug-ins\Blender\modules\speckle\serialization\base_object_serializer.py", line 218, in read_json
    return self.recompose_base(obj=obj)
  File "\\TAS-03\assets\3d modelling\Plug-ins\Blender\modules\speckle\serialization\base_object_serializer.py", line 276, in recompose_base
    base.__setattr__(prop, self.handle_value(value))
  File "\\TAS-03\assets\3d modelling\Plug-ins\Blender\modules\speckle\serialization\base_object_serializer.py", line 297, in handle_value
    obj_list = [self.handle_value(o) for o in obj]
  File "\\TAS-03\assets\3d modelling\Plug-ins\Blender\modules\speckle\serialization\base_object_serializer.py", line 297, in <listcomp>
    obj_list = [self.handle_value(o) for o in obj]
  File "\\TAS-03\assets\3d modelling\Plug-ins\Blender\modules\speckle\serialization\base_object_serializer.py", line 311, in handle_value
    return self.recompose_base(obj=obj)
  File "\\TAS-03\assets\3d modelling\Plug-ins\Blender\modules\speckle\serialization\base_object_serializer.py", line 276, in recompose_base
    base.__setattr__(prop, self.handle_value(value))
  File "\\TAS-03\assets\3d modelling\Plug-ins\Blender\modules\speckle\objects\geometry.py", line 352, in __setattr__
    super().__setattr__(name, value)
  File "\\TAS-03\assets\3d modelling\Plug-ins\Blender\modules\speckle\objects\base.py", line 96, in __setattr__
    super().__setattr__(name, value)
  File "C:\Users\tomsv\AppData\Roaming\Blender Foundation\Blender\2.92\scripts\addons\modules\pydantic\main.py", line 296, in __setattr__
    value, error_ = known_field.validate(value, self.dict(exclude={name}), loc=name, cls=self.__class__)

...

  File "C:\Users\tomsv\AppData\Roaming\Blender Foundation\Blender\2.92\scripts\addons\modules\pydantic\main.py", line 675, in _iter
    exclude_none=exclude_none,
  File "C:\Users\tomsv\AppData\Roaming\Blender Foundation\Blender\2.92\scripts\addons\modules\pydantic\main.py", line 582, in _get_value
    exclude_none=exclude_none,
  File "C:\Users\tomsv\AppData\Roaming\Blender Foundation\Blender\2.92\scripts\addons\modules\pydantic\main.py", line 343, in dict
    exclude_none=exclude_none,
  File "C:\Users\tomsv\AppData\Roaming\Blender Foundation\Blender\2.92\scripts\addons\modules\pydantic\main.py", line 333, in <dictcomp>
    return {
  File "C:\Users\tomsv\AppData\Roaming\Blender Foundation\Blender\2.92\scripts\addons\modules\pydantic\main.py", line 675, in _iter
    exclude_none=exclude_none,
  File "C:\Users\tomsv\AppData\Roaming\Blender Foundation\Blender\2.92\scripts\addons\modules\pydantic\main.py", line 574, in _get_value
    if isinstance(v, BaseModel):
  File "C:\Program Files\Blender Foundation\Blender 2.92\2.92\python\lib\abc.py", line 139, in __instancecheck__
    return _abc_instancecheck(cls, instance)
RecursionError: maximum recursion depth exceeded while calling a Python object
Error: Python script failed, check the message in the system console

Reproduction Steps & System Config (win, osx, web, etc.)

Example code above. Running on Windows 10.

Proposed Solution (if any)

Some small examples of querying available streams and receiving all objects from a stream would help to compare code.

Running:

res = client.object.get(stream_id=stream.id, object_id=commit.referencedObject)

works:

Base(id: a8f62919a00d274391d59abf44931457, speckle_type: Base, totalChildrenCount: 13)

However it is not entirely clear how to get a list of children objects from this result...

izzylys commented 3 years ago

Hey Tom!

Unfortunately, I'm not able to reproduce this error. The code you have written up should work just fine which makes me think either (1) the object you are retrieving is an edge case that I definitely want to look into or (2) Blender sets it's own lowered recursion limit for external python modules.

If possible, could you please share me on the stream and point me to the offending commit so I can see if I can reproduce the error with the same data? I would be curious to see it as I have previously received whole buildings constructed of lots of breps without running into recursion errors!

tsvilans commented 3 years ago

Sure thing. It's a simple stream I made using the web interface, and then uploaded a simple box to it from Rhino:

stream id: 1416bd66bb
branch: main
commit: ed05c70b05
tsvilans commented 3 years ago

Receiving the box in Rhino works fine, by the way.

tsvilans commented 3 years ago

Solved: was using an older version of pydantic. Upgraded by going to C:\Program Files\Blender Foundation\Blender 2.92\2.92\python\Scripts and doing pip install pydantic==1.7.2 --upgrade <Blender modules directory>.

izzylys commented 3 years ago

ah great! glad to hear it was a simple fix πŸ˜„

tsvilans commented 3 years ago

πŸ™ˆ back to boot camp.

A related question: how would you get a list of all children objects from that Base object? I see @Default in the object keys, but there are other keys as well, that don't correspond to children objects (speckle_type, units, __closure). Is the correct way to filter the key list for keys beginning with @?

izzylys commented 3 years ago

There isn't a set way to get the names of all child objects, though this is something we can add if it would be a helpful addition. There are a couple different methods for getting the property names on a base object: get_member_names, get_typed_member_names, and get_dynamic_member_names. You would probably want to use the first one to get all member names and filter them by type (inherits from Base).

Note that only dynamically added detached properties start with an @. Properties that are predefined on the object class can also be defined as detached. Eg, vertices and faces on a Mesh object are detached (making them children objects) but they are predefined on the Mesh class and do not start with an @. Check out the docs for more info on the py base object here.