Which Duet products are you using?
Firmware Version
3.7
Duet Web Control Version
3.7
Are you using a Single Board Computer (RaspberryPi) with your Duet?
Please upload the results of sending M122 in the gcode console.
Please upload the content of your config.g file.
Please upload the content of any other releveant macro files.
Details specific to your printer.
No response
Links to additional info.
No response
What happened?
See forum post here:
https://forum.duet3d.com/topic/39406/3.7.0-beta.3-dsf-python-callback-error/3
Environment: dsf-python 3.7.0b1, Python 3.13, Raspberry Pi (DSF in SBC mode), using SubscribeConnection(SubscriptionMode.PATCH) with get_object_model() / subscribe_to_keys().
In both cases below, DSF sends null for a value that the Python model says can't be null. The update then fails with an exception, so the object model can't be loaded or patched.
- inputs doesn't accept null entries
Where: dsf/object_model/inputs/inputs.py, class Inputs
DSF reports unused input channels as null in the inputs array. Inputs.init calls super().init(InputChannel) without allow_none=True. InputChannel also isn't declared as Optional, so the collection ends up with _allow_none = False. Loading an inputs array that contains null then fails in ModelCollection._coerce_item_value with:
TypeError: None is not allowed for collection of type <class '...InputChannel'>
Suggested fix: super().init(InputChannel, allow_none=True). That matches the C# model, where Inputs is a collection of nullable InputChannel.
Workaround:
from dsf.object_model.inputs import inputs as _inputs
_orig = _inputs.Inputs.init
def _init(self):
_orig(self)
self._allow_none = True
_inputs.Inputs.init = _init
2. job.file.customInfo doesn't accept null
Where: dsf/object_model/job/gcode_fileinfo.py
custom_info = model_prop("custom_info", ModelDictionary, ModelDictionary(False))
custom_info is declared with model_prop, which means it can't be null. When there is no job file, DSF sends a patch that sets job.file.customInfo to null. The model_prop setter passes None to _set_model_prop, and the patch fails.
Suggested fix: Either declare it with nullable_model_prop, or have the setter treat None as "clear the dictionary".
Workaround:
from dsf.object_model.job.gcode_fileinfo import GCodeFileInfo
_prop = GCodeFileInfo.custom_info
def _set(self, value):
if value is None:
self.custom_info.clear()
else:
_prop.fset(self, value)
GCodeFileInfo.custom_info = _prop.setter(_set)
Possible wider issue
Both bugs have the same cause: fields DSF can send as null are declared as non-nullable in Python. It may be worth checking the other model_prop and ModelCollection declarations against the C# model for nullability
Example script to reproduce the issue (workarounds commented out)
from dsf.connections import SubscribeConnection, SubscriptionMode
from dsf.object_model.inputs import inputs as _inputs
from dsf.object_model.job.gcode_fileinfo import GCodeFileInfo
import asyncio
'''
# Workaround: dsf-python 3.7.0b1 rejects null entries in the inputs collection,
# but DSF reports unused input channels as null
_orig_inputs_init = _inputs.Inputs.__init__
def _inputs_init(self):
_orig_inputs_init(self)
self._allow_none = True
_inputs.Inputs.__init__ = _inputs_init
# Workaround: dsf-python 3.7.0b1 declares job.file.customInfo as non-nullable,
# but DSF patches it to null when there is no job file. Treat null as "clear"
_custom_info_prop = GCodeFileInfo.custom_info
def _set_custom_info(self, value):
if value is None:
self.custom_info.clear()
else:
_custom_info_prop.fset(self, value)
GCodeFileInfo.custom_info = _custom_info_prop.setter(_set_custom_info)
'''
def handle_change(*, key, data, indices):
print(key, data, indices)
subscription = SubscribeConnection(SubscriptionMode.PATCH)
subscription.connect()
object_model = subscription.get_object_model()
#object_model.update_from_json({"job.file.customInfo": ["Test=T1"]})
#print(object_model.job.file.customInfo)
unsubscribe = subscription.subscribe_to_keys(
["state.status"],
handle_change,
)
async def main():
while True:
print('check')
object_model = subscription.get_object_model()
await asyncio.sleep(5)
asyncio.run(main())
Which Duet products are you using?
Firmware Version
3.7
Duet Web Control Version
3.7
Are you using a Single Board Computer (RaspberryPi) with your Duet?
Please upload the results of sending M122 in the gcode console.
Please upload the content of your config.g file.
Please upload the content of any other releveant macro files.
Details specific to your printer.
No response
Links to additional info.
No response
What happened?
See forum post here:
https://forum.duet3d.com/topic/39406/3.7.0-beta.3-dsf-python-callback-error/3
Environment: dsf-python 3.7.0b1, Python 3.13, Raspberry Pi (DSF in SBC mode), using SubscribeConnection(SubscriptionMode.PATCH) with get_object_model() / subscribe_to_keys().
In both cases below, DSF sends null for a value that the Python model says can't be null. The update then fails with an exception, so the object model can't be loaded or patched.
Where: dsf/object_model/inputs/inputs.py, class Inputs
DSF reports unused input channels as null in the inputs array. Inputs.init calls super().init(InputChannel) without allow_none=True. InputChannel also isn't declared as Optional, so the collection ends up with _allow_none = False. Loading an inputs array that contains null then fails in ModelCollection._coerce_item_value with:
TypeError: None is not allowed for collection of type <class '...InputChannel'>
Suggested fix: super().init(InputChannel, allow_none=True). That matches the C# model, where Inputs is a collection of nullable InputChannel.
Workaround:
from dsf.object_model.inputs import inputs as _inputs
_orig = _inputs.Inputs.init
def _init(self):
_orig(self)
self._allow_none = True
_inputs.Inputs.init = _init
2. job.file.customInfo doesn't accept null
Where: dsf/object_model/job/gcode_fileinfo.py
custom_info = model_prop("custom_info", ModelDictionary, ModelDictionary(False))
custom_info is declared with model_prop, which means it can't be null. When there is no job file, DSF sends a patch that sets job.file.customInfo to null. The model_prop setter passes None to _set_model_prop, and the patch fails.
Suggested fix: Either declare it with nullable_model_prop, or have the setter treat None as "clear the dictionary".
Workaround:
from dsf.object_model.job.gcode_fileinfo import GCodeFileInfo
_prop = GCodeFileInfo.custom_info
def _set(self, value):
if value is None:
self.custom_info.clear()
else:
_prop.fset(self, value)
GCodeFileInfo.custom_info = _prop.setter(_set)
Possible wider issue
Both bugs have the same cause: fields DSF can send as null are declared as non-nullable in Python. It may be worth checking the other model_prop and ModelCollection declarations against the C# model for nullability
Example script to reproduce the issue (workarounds commented out)