From 742a2c61f2b469b49481f804905798862110fafa Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:17:34 +0100
Subject: [PATCH 01/19] feat: improve type hinting
---
src/dsf/commands/code_parameter.py | 8 ++---
src/dsf/connections/base_connection.py | 16 ++++++++--
src/dsf/object_model/model_collection.py | 22 +++++++-------
src/dsf/object_model/model_dictionary.py | 37 ++++++++++--------------
src/dsf/object_model/model_object.py | 4 +--
src/dsf/object_model/model_type.py | 4 +--
src/dsf/object_model/utils.py | 31 +++++++++-----------
src/dsf/utils.py | 20 ++++++-------
8 files changed, 71 insertions(+), 71 deletions(-)
diff --git a/src/dsf/commands/code_parameter.py b/src/dsf/commands/code_parameter.py
index 4f781ee..5e2fcf2 100644
--- a/src/dsf/commands/code_parameter.py
+++ b/src/dsf/commands/code_parameter.py
@@ -196,10 +196,10 @@ def as_int_array(self) -> list[int]:
parsed_value: object = self.__parsed_value
if isinstance(parsed_value, list):
if isinstance(parsed_value[0], DriverId):
- values = cast(list[DriverId], parsed_value)
- return [int(value.as_int()) for value in values]
- values = cast(list[int] | list[float], parsed_value)
- return [int(value) for value in values]
+ driver_ids = cast(list[DriverId], parsed_value)
+ return [int(driver_id.as_int()) for driver_id in driver_ids]
+ numbers = cast(list[int] | list[float], parsed_value)
+ return [int(value) for value in numbers]
if isinstance(parsed_value, int):
return [parsed_value]
if isinstance(parsed_value, DriverId):
diff --git a/src/dsf/connections/base_connection.py b/src/dsf/connections/base_connection.py
index 8f496f1..9fda6c1 100644
--- a/src/dsf/connections/base_connection.py
+++ b/src/dsf/connections/base_connection.py
@@ -2,7 +2,7 @@
import select
import socket
import time
-from typing import Optional, overload
+from typing import Any, Optional, Protocol, Self, TypeVar, overload
from .exceptions import IncompatibleVersionException, InternalServerException, TaskCanceledException
from .init_messages import client_init_messages, server_init_message
@@ -12,6 +12,16 @@
from ..object_model.model_object import TModelObject
from ..utils import JSONElement
+
+class _JSONDeserializable(Protocol):
+ @classmethod
+ def from_json(cls, data: Any) -> Self:
+ ...
+
+
+TDeserializable = TypeVar("TDeserializable", bound=_JSONDeserializable)
+
+
class BaseConnection:
"""
Base class for connections that access the control server via the Duet API
@@ -22,7 +32,7 @@ def __init__(self, debug: bool = False, timeout: int = 3):
self.debug = debug
self.timeout = timeout
self.socket: Optional[socket.socket] = None
- self.id = None
+ self.id: Optional[int] = None
self.input = ""
def _connect(self, init_message: client_init_messages.ClientInitMessage, socket_file: str):
@@ -100,7 +110,7 @@ def send(self, msg: object):
if self.socket:
self.socket.sendall(json_string.encode("utf8"))
- def receive(self, cls: type[TModelObject]) -> TModelObject:
+ def receive(self, cls: type[TDeserializable]) -> TDeserializable:
"""Receive a deserialized object from the server"""
json_string = self.receive_json()
return cls.from_json(json.loads(json_string))
diff --git a/src/dsf/object_model/model_collection.py b/src/dsf/object_model/model_collection.py
index 9c9eb67..44873e1 100644
--- a/src/dsf/object_model/model_collection.py
+++ b/src/dsf/object_model/model_collection.py
@@ -1,5 +1,5 @@
from enum import Enum
-from typing import Generic, TypeVar, List, Dict, Any, Union, Optional, get_origin, get_args, cast
+from typing import Callable, Generic, TypeVar, List, Dict, Any, Union, Optional, get_origin, get_args, cast
from ..utils import JSONElement
from .model_type import ModelType
from .model_dictionary import ModelDictionary
@@ -41,11 +41,11 @@ def __init__(self, item_constructor: type[T] | object, value: Optional[List[T]]
self._runtime_model_type = cast(type[object], item_origin or item_constructor)
if isinstance(resolved_constructor, type):
- self._item_constructor: type[T] = cast(type[T], resolved_constructor)
+ self._item_constructor: Callable[..., T] = cast(Callable[..., T], resolved_constructor)
elif isinstance(self._runtime_model_type, type):
- self._item_constructor = cast(type[T], self._runtime_model_type)
+ self._item_constructor = cast(Callable[..., T], self._runtime_model_type)
else:
- self._item_constructor = cast(type[T], object)
+ self._item_constructor = cast(Callable[..., T], object)
if value is not None:
self[:] = []
@@ -55,7 +55,7 @@ def __init__(self, item_constructor: type[T] | object, value: Optional[List[T]]
continue
if isinstance(item, self._runtime_model_type):
- self.append(item)
+ self.append(cast(T, item))
else:
ref_item = self._create_item(index)
if not is_model_object(ref_item):
@@ -64,7 +64,7 @@ def __init__(self, item_constructor: type[T] | object, value: Optional[List[T]]
# if issubclass(self._item_constructor, ModelType[T]):
ref_item.update_from_json(item)
- self.append(ref_item)
+ self.append(cast(T, ref_item))
@classmethod
def from_json(cls, data: list[JSONElement]):
@@ -117,7 +117,7 @@ def update_from_json(self, data: list[JSONElement]) -> 'ModelCollection[T]':
# If the new item data is null, set the current item to null (even if it was a model object before)
if new_item_data is None and self._allow_none:
- self[i] = None
+ self[i] = cast(T, None)
continue
# If the current item is null then we need to create a new item
@@ -132,12 +132,12 @@ def update_from_json(self, data: list[JSONElement]) -> 'ModelCollection[T]':
ref_item = None
if ref_item is not None and is_model_object(ref_item):
- self[i] = cast(T, cast(ModelType[JSONElement], ref_item).update_from_json(new_item_data))
+ self[i] = cast(T, ref_item.update_from_json(new_item_data))
else:
self[i] = self._coerce_item_value(new_item_data)
# Use the `update_from_json` method of the current item if it's a model object, otherwise replace it with the new data
elif is_model_object(current_item):
- self[i] = cast(T, cast(ModelType[JSONElement], current_item).update_from_json(new_item_data))
+ self[i] = cast(T, current_item.update_from_json(new_item_data))
else:
self[i] = self._coerce_item_value(new_item_data)
@@ -149,14 +149,14 @@ def update_from_json(self, data: list[JSONElement]) -> 'ModelCollection[T]':
elif isinstance(item_to_add, self._runtime_model_type):
self.append(cast(T, item_to_add))
else:
- ref_item: Optional[T] = None
+ ref_item = None
try:
ref_item = self._create_item(i)
except TypeError:
ref_item = None
if ref_item is not None and is_model_object(ref_item):
- self.append(cast(ModelType[JSONElement], ref_item).update_from_json(item_to_add))
+ self.append(cast(T, ref_item.update_from_json(item_to_add)))
else:
self.append(self._coerce_item_value(item_to_add))
diff --git a/src/dsf/object_model/model_dictionary.py b/src/dsf/object_model/model_dictionary.py
index 8e0a3bf..04001de 100644
--- a/src/dsf/object_model/model_dictionary.py
+++ b/src/dsf/object_model/model_dictionary.py
@@ -1,19 +1,19 @@
-from typing import Optional, TypeVar, Self
+from typing import Any, Optional, Self, TYPE_CHECKING
-from .model_object import TModelObject
from .model_type import ModelType
-from ..utils import JSONObj, JSONElement, preserve_builtin
+from ..utils import JSONObj
-T = TypeVar("T")
+if TYPE_CHECKING:
+ from .model_object import ModelObject
-class ModelDictionary(ModelType[JSONObj], JSONObj):
+class ModelDictionary(ModelType[JSONObj], dict[str, Any]):
"""
Class for storing model object items in a dictionary
Useful for updating model object items from JSON data (patches)
"""
- def __init__(self, null_deletes_keys: bool, item_constructor: Optional[type[TModelObject]] = None, value: Optional[JSONObj | TModelObject] = None):
+ def __init__(self, null_deletes_keys: bool, item_constructor: Optional[type["ModelObject"]] = None, value: Optional[dict[str, Any]] = None):
"""
:param null_deletes_keys: Whether setting null to items effectively deletes them
:param item_constructor: Item constructor type to use for type-checking
@@ -29,39 +29,32 @@ def __init__(self, null_deletes_keys: bool, item_constructor: Optional[type[TMod
for k, v in value.items():
self[k] = v
- def __setitem__(self, key: str, value: JSONElement) -> Self | None:
+ def __setitem__(self, key: str, value: Any) -> None:
from .utils import is_model_object
if value is None:
if self._null_deletes_keys:
self.pop(key, None)
- return self
+ return
return super().__setitem__(key, value)
current_item = self.get(key)
if current_item is None and self._item_constructor:
+ if not isinstance(value, dict):
+ raise TypeError(f"Value for key '{key}' must be of type dict to update the model object. Got {type(value)}: {value}")
new_item = self._item_constructor()
- if is_model_object(new_item):
- if not isinstance(value, dict):
- raise TypeError(f"Value for key '{key}' must be of type dict to update the model object. Got {type(value)}: {value}")
- updated_item = new_item.update_from_json(value)
- return super().__setitem__(key, updated_item)
- else:
- ref_item = self._item_constructor()
- new_item = ref_item.update_from_json(value)
- return super().__setitem__(key, new_item)
+ return super().__setitem__(key, new_item.update_from_json(value))
elif is_model_object(current_item):
- new_item = current_item.update_from_json(value)
- return super().__setitem__(key, new_item)
+ return super().__setitem__(key, current_item.update_from_json(value))
return super().__setitem__(key, value)
@classmethod
- def from_json(cls, data: JSONObj):
+ def from_json(cls, data: Optional[JSONObj]) -> Self:
"""Deserialize a new instance of this class from JSON deserialized dictionary"""
- return cls().update_from_json(**preserve_builtin(data))
+ return cls(False).update_from_json(data)
- def update_from_json(self, data: JSONObj):
+ def update_from_json(self, data: Optional[JSONObj]) -> Self:
if data is None:
super().clear()
else:
diff --git a/src/dsf/object_model/model_object.py b/src/dsf/object_model/model_object.py
index 72c7aae..5091942 100644
--- a/src/dsf/object_model/model_object.py
+++ b/src/dsf/object_model/model_object.py
@@ -3,7 +3,7 @@
from typing import TypeVar, Any, Union, cast
from .model_type import ModelType
-from ..utils import preserve_builtin, camel_to_snake, snake_to_camel, JSONObj
+from ..utils import preserve_builtin, camel_to_snake, snake_to_camel, JSONElement, JSONObj
class FloatJSON(float):
@@ -48,7 +48,7 @@ def __json_serialize(obj: object) -> str | JSONObj:
# also convert back 'globals' to 'global'
return {snake_to_camel(str(k) if k != '_globals' else '_global'): v for k, v in obj.__dict__.items()}
- def _update_from_json(self: TModelObject, **kwargs: object) -> TModelObject:
+ def _update_from_json(self: TModelObject, **kwargs: JSONElement) -> TModelObject:
"""Update this instance from a given JSON element
This method iterate over all writeable properties to update them.
It means classes with get-only properties should override this method in order to update them.
diff --git a/src/dsf/object_model/model_type.py b/src/dsf/object_model/model_type.py
index 42356f4..5d5b6ce 100644
--- a/src/dsf/object_model/model_type.py
+++ b/src/dsf/object_model/model_type.py
@@ -2,13 +2,13 @@
from ..utils import JSONElement
-T = TypeVar("T", bound=JSONElement)
+T = TypeVar("T", bound=JSONElement, contravariant=True)
TModelValue: TypeAlias = "ModelType[T]"
@runtime_checkable
class ModelType(Protocol[T]):
@classmethod
- def from_json(cls: type[T], data: T) -> T:
+ def from_json(cls, data: T) -> Self:
"""Deserialize a new instance of this class from JSON deserialized dictionary"""
...
diff --git a/src/dsf/object_model/utils.py b/src/dsf/object_model/utils.py
index d23b4ed..c898549 100644
--- a/src/dsf/object_model/utils.py
+++ b/src/dsf/object_model/utils.py
@@ -1,10 +1,11 @@
import copy
from datetime import datetime
-from typing import Optional, Protocol, TypeVar, Union, Callable, cast, get_origin, overload
+from typing import Any, Optional, Protocol, TypeVar, TypeGuard, Union, Callable, cast, get_origin, overload
from .model_object import ModelObject
from .model_collection import ModelCollection
from .model_dictionary import ModelDictionary
+from .model_type import ModelType
from ..utils import JSONObj, JSONElement
@@ -24,7 +25,7 @@ def __get__(self, obj: object, objtype: Optional[type[object]] = None) -> _TProp
T = TypeVar("T")
-def is_model_object(o: object) -> bool:
+def is_model_object(o: object) -> TypeGuard[ModelType[Any]]:
from .model_object import ModelObject
from .model_collection import ModelCollection
from .model_dictionary import ModelDictionary
@@ -32,9 +33,9 @@ def is_model_object(o: object) -> bool:
return isinstance(o, ModelObject) or isinstance(o, ModelCollection) or isinstance(o, ModelDictionary)
-def _set_model_prop(instance: object, name: str, runtime_type: type[JSONElement | datetime], current_value: T, value: Union[T, JSONElement]):
+def _set_model_prop(instance: object, name: str, runtime_type: type[object], current_value: T, value: Union[T, JSONElement]) -> None:
if value is None and isinstance(current_value, ModelDictionary): # DSF sends null to clear a dictionary
- current_value.update_from_json(cast(JSONObj, None))
+ current_value.update_from_json(None)
elif isinstance(value, dict): # Update from JSON
if not isinstance(current_value, (ModelObject, ModelDictionary)):
raise TypeError(f"{instance.__class__.__name__}.{name} must be of type ModelObject or ModelDictionary to update from a dict."
@@ -52,8 +53,8 @@ def _set_model_prop(instance: object, name: str, runtime_type: type[JSONElement
except ValueError:
raise TypeError(f"{instance.__class__.__name__}.{name} must be a valid ISO format datetime string to update from JSON. Got: {value}")
elif isinstance(value, (str, int, float, bool)):
- value = runtime_type(value) # ignore type
- setattr(instance, name, value)
+ converter = cast(Callable[[object], object], runtime_type)
+ setattr(instance, name, converter(value))
else:
raise TypeError(f"{instance.__class__.__name__}.{name} must be of type {runtime_type} or a compatible JSON element to update from."
f" Got {type(value).__name__}: {value}")
@@ -84,16 +85,14 @@ def _make_default() -> T:
return cast(T, copy.deepcopy(default))
return cast(T, _scalar_default)
- @property
- def prop(self: object) -> T:
+ def getter(self: object) -> T:
v = getattr(self, STORAGE_NAME, None)
if v is None:
v = _make_default()
setattr(self, STORAGE_NAME, v)
return v
- @prop.setter
- def prop(self: object, value: Union[T, JSONElement]) -> None:
+ def setter(self: object, value: Union[T, JSONElement]) -> None:
def get_or_create_value() -> T:
current_value: Optional[T] = getattr(self, STORAGE_NAME, None)
if current_value is None:
@@ -108,9 +107,9 @@ def get_or_create_value() -> T:
current_value = get_or_create_value()
_set_model_prop(self, STORAGE_NAME, runtime_model_type, current_value, value)
- return cast(TypedReadableProperty[T], prop)
+ return cast(TypedReadableProperty[T], property(getter, setter))
-def nullable_model_prop(name: str, model_type: type[T], constructor: Optional[Callable[[], T]] = None) -> TypedReadableProperty[Optional[T]]:
+def nullable_model_prop(name: str, model_type: type[T], constructor: Optional[Callable[[], Optional[T]]] = None) -> TypedReadableProperty[Optional[T]]:
"""
Wrap a nullable model object property so that type checks can be performed during update
:param name: Property of the derived class
@@ -132,12 +131,10 @@ def nullable_model_prop(name: str, model_type: type[T], constructor: Optional[Ca
else:
_factory = constructor
- @property
- def prop(self: object) -> Optional[T]:
+ def getter(self: object) -> Optional[T]:
return getattr(self, STORAGE_NAME, None)
- @prop.setter
- def prop(self: object, value: Union[T, JSONElement, None]) -> None:
+ def setter(self: object, value: Union[T, JSONElement, None]) -> None:
def get_or_create_value() -> Optional[T]:
v = getattr(self, STORAGE_NAME, None)
if v is None:
@@ -153,4 +150,4 @@ def get_or_create_value() -> Optional[T]:
current_value = get_or_create_value()
_set_model_prop(self, STORAGE_NAME, runtime_model_type, current_value, value)
- return cast(TypedReadableProperty[Optional[T]], prop)
+ return cast(TypedReadableProperty[Optional[T]], property(getter, setter))
diff --git a/src/dsf/utils.py b/src/dsf/utils.py
index d1cee10..5f48b4d 100644
--- a/src/dsf/utils.py
+++ b/src/dsf/utils.py
@@ -29,34 +29,34 @@ def _matches_type(value: Any, expected_type: Any) -> bool:
if len(args) != 2:
return True
key_type, val_type = args
- typed_value = cast(dict[Any, Any], value)
- return all(_matches_type(k, key_type) and _matches_type(v, val_type) for k, v in typed_value.items())
+ typed_dict = cast(dict[Any, Any], value)
+ return all(_matches_type(k, key_type) and _matches_type(v, val_type) for k, v in typed_dict.items())
if origin is list:
if not isinstance(value, list):
return False
if len(args) != 1:
return True
- typed_value = cast(list[Any], value)
- return all(_matches_type(item, args[0]) for item in typed_value)
+ typed_list = cast(list[Any], value)
+ return all(_matches_type(item, args[0]) for item in typed_list)
if origin is tuple:
if not isinstance(value, tuple):
return False
- typed_value = cast(tuple[Any, ...], value)
+ typed_tuple = cast(tuple[Any, ...], value)
if len(args) == 2 and args[1] is Ellipsis:
- return all(_matches_type(item, args[0]) for item in typed_value)
- if len(args) != len(typed_value):
+ return all(_matches_type(item, args[0]) for item in typed_tuple)
+ if len(args) != len(typed_tuple):
return False
- return all(_matches_type(item, item_type) for item, item_type in zip(typed_value, args))
+ return all(_matches_type(item, item_type) for item, item_type in zip(typed_tuple, args))
if origin is set:
if not isinstance(value, set):
return False
if len(args) != 1:
return True
- typed_value = cast(set[Any], value)
- return all(_matches_type(item, args[0]) for item in typed_value)
+ typed_set = cast(set[Any], value)
+ return all(_matches_type(item, args[0]) for item in typed_set)
try:
return isinstance(value, expected_type)
From 3927b903556c271d20417ee36499d9798d7c8e13 Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:22:56 +0100
Subject: [PATCH 02/19] chore: remove unused imports
---
.../connections/init_messages/server_init_message.py | 2 +-
src/dsf/object_model/job/build.py | 2 --
src/dsf/object_model/job/gcode_fileinfo.py | 1 -
src/dsf/object_model/job/job.py | 2 --
src/dsf/object_model/job/times_left.py | 2 --
src/dsf/object_model/messages/messages.py | 1 -
src/dsf/object_model/model_collection.py | 4 +---
src/dsf/object_model/move/axis.py | 1 -
src/dsf/object_model/move/input_shaping.py | 1 -
src/dsf/object_model/move/keepout_zone.py | 2 --
.../object_model/move/kinematics/delta_kinematics.py | 2 --
.../move/kinematics/hangprinter_kinematics.py | 2 --
src/dsf/object_model/move/kinematics/kinematics.py | 2 --
.../object_model/move/kinematics/tilt_correction.py | 2 --
src/dsf/object_model/move/move.py | 2 --
src/dsf/object_model/move/move_compensation.py | 1 -
src/dsf/object_model/network/network_interface.py | 3 ---
src/dsf/object_model/object_model.py | 2 +-
src/dsf/object_model/plugins/plugin_manifest.py | 1 -
src/dsf/object_model/sensors/analog_sensor.py | 2 --
src/dsf/object_model/state/message_box.py | 1 -
src/dsf/object_model/state/restore_point.py | 3 ---
src/dsf/object_model/tools/tools.py | 3 ---
tests/test_custom_http_endpoint.py | 3 ---
tests/test_custom_m_codes.py | 5 -----
tests/test_object_model.py | 11 ++++++-----
tests/test_send_simple_code.py | 5 -----
tests/test_subscribe_object_model.py | 5 +----
28 files changed, 10 insertions(+), 63 deletions(-)
diff --git a/src/dsf/connections/init_messages/server_init_message.py b/src/dsf/connections/init_messages/server_init_message.py
index 49fae60..814d36b 100644
--- a/src/dsf/connections/init_messages/server_init_message.py
+++ b/src/dsf/connections/init_messages/server_init_message.py
@@ -19,7 +19,7 @@
"""
from ... import PROTOCOL_VERSION
-from ...utils import preserve_builtin, JSONObj
+from ...utils import JSONObj
class ServerInitMessage:
diff --git a/src/dsf/object_model/job/build.py b/src/dsf/object_model/job/build.py
index 784c809..f10b625 100644
--- a/src/dsf/object_model/job/build.py
+++ b/src/dsf/object_model/job/build.py
@@ -1,5 +1,3 @@
-from typing import List
-
from .build_object import BuildObject
from ..model_collection import ModelCollection
from ..model_object import ModelObject
diff --git a/src/dsf/object_model/job/gcode_fileinfo.py b/src/dsf/object_model/job/gcode_fileinfo.py
index 8cda98a..fbc5374 100644
--- a/src/dsf/object_model/job/gcode_fileinfo.py
+++ b/src/dsf/object_model/job/gcode_fileinfo.py
@@ -1,5 +1,4 @@
from datetime import datetime
-from typing import List, Union
from .thumbnail_info import ThumbnailInfo
from ..model_collection import ModelCollection
diff --git a/src/dsf/object_model/job/job.py b/src/dsf/object_model/job/job.py
index 437de9c..968f249 100644
--- a/src/dsf/object_model/job/job.py
+++ b/src/dsf/object_model/job/job.py
@@ -1,5 +1,3 @@
-from typing import List, Union
-
from .build import Build
from .gcode_fileinfo import GCodeFileInfo
from .layer import Layer
diff --git a/src/dsf/object_model/job/times_left.py b/src/dsf/object_model/job/times_left.py
index 7296138..cd2411d 100644
--- a/src/dsf/object_model/job/times_left.py
+++ b/src/dsf/object_model/job/times_left.py
@@ -1,5 +1,3 @@
-from typing import Union
-
from ..model_object import ModelObject
from ..utils import nullable_model_prop
diff --git a/src/dsf/object_model/messages/messages.py b/src/dsf/object_model/messages/messages.py
index 616087f..be3ddff 100644
--- a/src/dsf/object_model/messages/messages.py
+++ b/src/dsf/object_model/messages/messages.py
@@ -1,4 +1,3 @@
-import dateutil.parser as dp
from datetime import datetime
from enum import IntEnum
diff --git a/src/dsf/object_model/model_collection.py b/src/dsf/object_model/model_collection.py
index 44873e1..5f851df 100644
--- a/src/dsf/object_model/model_collection.py
+++ b/src/dsf/object_model/model_collection.py
@@ -1,9 +1,7 @@
from enum import Enum
-from typing import Callable, Generic, TypeVar, List, Dict, Any, Union, Optional, get_origin, get_args, cast
+from typing import Callable, Generic, TypeVar, List, Union, Optional, get_origin, get_args, cast
from ..utils import JSONElement
from .model_type import ModelType
-from .model_dictionary import ModelDictionary
-from .model_object import ModelObject
T = TypeVar('T')
# T = TypeVar('T', bound=Union[JSONElement, "ModelCollection[Any]", ModelDictionary, ModelObject])
diff --git a/src/dsf/object_model/move/axis.py b/src/dsf/object_model/move/axis.py
index 6130fc7..aabbbcb 100644
--- a/src/dsf/object_model/move/axis.py
+++ b/src/dsf/object_model/move/axis.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import List, Optional
from .driver_id import DriverId
from .microstepping import MicroStepping
diff --git a/src/dsf/object_model/move/input_shaping.py b/src/dsf/object_model/move/input_shaping.py
index 2bfc8c7..07bb9c0 100644
--- a/src/dsf/object_model/move/input_shaping.py
+++ b/src/dsf/object_model/move/input_shaping.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import List
from ..model_object import ModelObject
diff --git a/src/dsf/object_model/move/keepout_zone.py b/src/dsf/object_model/move/keepout_zone.py
index f51280a..9da8904 100644
--- a/src/dsf/object_model/move/keepout_zone.py
+++ b/src/dsf/object_model/move/keepout_zone.py
@@ -1,5 +1,3 @@
-from typing import List
-
from ..model_collection import ModelCollection
from ..model_object import ModelObject
from ..utils import model_prop
diff --git a/src/dsf/object_model/move/kinematics/delta_kinematics.py b/src/dsf/object_model/move/kinematics/delta_kinematics.py
index 22240d4..2834912 100644
--- a/src/dsf/object_model/move/kinematics/delta_kinematics.py
+++ b/src/dsf/object_model/move/kinematics/delta_kinematics.py
@@ -1,5 +1,3 @@
-from typing import List
-
from .delta_tower import DeltaTower
from .kinematics import Kinematics
from .kinematics_name import KinematicsName
diff --git a/src/dsf/object_model/move/kinematics/hangprinter_kinematics.py b/src/dsf/object_model/move/kinematics/hangprinter_kinematics.py
index 7edf353..c651ab8 100644
--- a/src/dsf/object_model/move/kinematics/hangprinter_kinematics.py
+++ b/src/dsf/object_model/move/kinematics/hangprinter_kinematics.py
@@ -1,5 +1,3 @@
-from typing import List
-
from .kinematics import Kinematics
from .kinematics_name import KinematicsName
from ...utils import model_prop
diff --git a/src/dsf/object_model/move/kinematics/kinematics.py b/src/dsf/object_model/move/kinematics/kinematics.py
index bdb7db5..ce56721 100644
--- a/src/dsf/object_model/move/kinematics/kinematics.py
+++ b/src/dsf/object_model/move/kinematics/kinematics.py
@@ -1,5 +1,3 @@
-from typing import Union
-
from .kinematics_name import KinematicsName
from ..move_segmentation import MoveSegmentation
from ...model_object import ModelObject
diff --git a/src/dsf/object_model/move/kinematics/tilt_correction.py b/src/dsf/object_model/move/kinematics/tilt_correction.py
index 398dd38..62af3da 100644
--- a/src/dsf/object_model/move/kinematics/tilt_correction.py
+++ b/src/dsf/object_model/move/kinematics/tilt_correction.py
@@ -1,5 +1,3 @@
-from typing import List
-
from ...model_object import ModelObject
from ...model_collection import ModelCollection
from ...utils import model_prop
diff --git a/src/dsf/object_model/move/move.py b/src/dsf/object_model/move/move.py
index bba6409..0e685fd 100644
--- a/src/dsf/object_model/move/move.py
+++ b/src/dsf/object_model/move/move.py
@@ -1,5 +1,3 @@
-from typing import List
-
from .axis import Axis
from .current_move import CurrentMove
from .extruder import Extruder
diff --git a/src/dsf/object_model/move/move_compensation.py b/src/dsf/object_model/move/move_compensation.py
index a98b6c0..6cc6389 100644
--- a/src/dsf/object_model/move/move_compensation.py
+++ b/src/dsf/object_model/move/move_compensation.py
@@ -1,5 +1,4 @@
from enum import Enum
-from typing import Union
from .move_calibration import MoveDeviations
from .probe_grid import ProbeGrid
diff --git a/src/dsf/object_model/network/network_interface.py b/src/dsf/object_model/network/network_interface.py
index 270179a..e0f33fc 100644
--- a/src/dsf/object_model/network/network_interface.py
+++ b/src/dsf/object_model/network/network_interface.py
@@ -1,12 +1,9 @@
-from typing import List, Union
-
from .network_interface_type import NetworkInterfaceType
from .network_protocol import NetworkProtocol
from .network_state import NetworkState
from ..model_object import ModelObject
from ..model_collection import ModelCollection
from ..utils import nullable_model_prop, model_prop
-from ...utils import deprecated
class NetworkInterface(ModelObject):
diff --git a/src/dsf/object_model/object_model.py b/src/dsf/object_model/object_model.py
index 26b0f2f..2946d71 100644
--- a/src/dsf/object_model/object_model.py
+++ b/src/dsf/object_model/object_model.py
@@ -1,4 +1,4 @@
-from typing import List, Optional
+from typing import Optional
from .model_collection import ModelCollection
from .model_dictionary import ModelDictionary
diff --git a/src/dsf/object_model/plugins/plugin_manifest.py b/src/dsf/object_model/plugins/plugin_manifest.py
index c3ad959..b90ed2c 100644
--- a/src/dsf/object_model/plugins/plugin_manifest.py
+++ b/src/dsf/object_model/plugins/plugin_manifest.py
@@ -1,5 +1,4 @@
import re
-from typing import List, Union
from .sbc_permissions import SbcPermissions
from ..model_object import ModelObject
diff --git a/src/dsf/object_model/sensors/analog_sensor.py b/src/dsf/object_model/sensors/analog_sensor.py
index 7de5dd1..3266bfc 100644
--- a/src/dsf/object_model/sensors/analog_sensor.py
+++ b/src/dsf/object_model/sensors/analog_sensor.py
@@ -1,5 +1,3 @@
-from typing import Union
-
from .analog_sensor_type import AnalogSensorType
from .temperature_error import TemperatureError
from ..model_object import ModelObject
diff --git a/src/dsf/object_model/state/message_box.py b/src/dsf/object_model/state/message_box.py
index db11d74..4d8c2d0 100644
--- a/src/dsf/object_model/state/message_box.py
+++ b/src/dsf/object_model/state/message_box.py
@@ -1,5 +1,4 @@
from enum import IntEnum
-from typing import List, Union
from ..model_object import ModelObject
from ..model_collection import ModelCollection
from ..utils import model_prop, nullable_model_prop
diff --git a/src/dsf/object_model/state/restore_point.py b/src/dsf/object_model/state/restore_point.py
index cdb4637..2cc60a7 100644
--- a/src/dsf/object_model/state/restore_point.py
+++ b/src/dsf/object_model/state/restore_point.py
@@ -1,6 +1,3 @@
-from typing import List, Union
-
-
from ..model_object import ModelObject
from ..model_collection import ModelCollection
from ..utils import model_prop, nullable_model_prop
diff --git a/src/dsf/object_model/tools/tools.py b/src/dsf/object_model/tools/tools.py
index 0319acc..510e4be 100644
--- a/src/dsf/object_model/tools/tools.py
+++ b/src/dsf/object_model/tools/tools.py
@@ -1,11 +1,8 @@
-from typing import List
-
from .tool_state import ToolState
from .tool_retraction import ToolRetraction
from ..model_object import ModelObject
from ..model_collection import ModelCollection
from ..utils import model_prop, nullable_model_prop
-from ...utils import deprecated
class Tool(ModelObject):
diff --git a/tests/test_custom_http_endpoint.py b/tests/test_custom_http_endpoint.py
index 3b3c3b7..01f305f 100644
--- a/tests/test_custom_http_endpoint.py
+++ b/tests/test_custom_http_endpoint.py
@@ -1,13 +1,10 @@
import threading
import os
-import pathlib
import socket
import time
-import importlib.util
import unittest
import json
import tempfile
-import requests
from src.dsf import PROTOCOL_VERSION
from src.dsf.connections import CommandConnection
diff --git a/tests/test_custom_m_codes.py b/tests/test_custom_m_codes.py
index bffceb4..bd6e505 100644
--- a/tests/test_custom_m_codes.py
+++ b/tests/test_custom_m_codes.py
@@ -1,18 +1,13 @@
import threading
import os
-import pathlib
import socket
-import time
-import importlib.util
import json
import unittest
import tempfile
-from unittest.mock import patch
from src.dsf import PROTOCOL_VERSION
from src.dsf.connections import InterceptConnection, InterceptionMode
from src.dsf.commands.code import CodeType
-from src.dsf.object_model import MessageType
class TestCustomMCodes(unittest.TestCase):
diff --git a/tests/test_object_model.py b/tests/test_object_model.py
index 4048b67..94a2e8f 100644
--- a/tests/test_object_model.py
+++ b/tests/test_object_model.py
@@ -7,11 +7,12 @@
from src.dsf.object_model.utils import is_model_object, JSONElement, JSONObj, model_prop, nullable_model_prop
from src.dsf.object_model.object_model import ModelCollection, ModelDictionary, ModelObject
+class SubModel(ModelObject):
+ value = model_prop("value", int, 0)
+
+
class TestModelObject(unittest.TestCase):
class Dummy(ModelObject):
- class SubModel(ModelObject):
- value = model_prop("value", int, 0)
-
p_int = model_prop("p_int", int, 1)
np_int = nullable_model_prop("np_int", int)
@@ -26,8 +27,8 @@ class SubModel(ModelObject):
p_model_collection = model_prop("p_model_collection", ModelCollection[SubModel], ModelCollection(SubModel))
p_model_ncollection = model_prop("p_model_ncollection", ModelCollection[Optional[SubModel]], ModelCollection(Optional[SubModel]))
- np_model_collection = nullable_model_prop("np_model_collection", ModelCollection[SubModel], lambda sub_model=SubModel: ModelCollection(sub_model))
- np_model_ncollection = nullable_model_prop("np_model_ncollection", ModelCollection[Optional[SubModel]], lambda sub_model=SubModel: ModelCollection(Optional[sub_model]))
+ np_model_collection = nullable_model_prop("np_model_collection", ModelCollection[SubModel], lambda: ModelCollection(SubModel))
+ np_model_ncollection = nullable_model_prop("np_model_ncollection", ModelCollection[Optional[SubModel]], lambda: ModelCollection(Optional[SubModel]))
def setUp(self):
pass
diff --git a/tests/test_send_simple_code.py b/tests/test_send_simple_code.py
index de987c4..f6f9b6e 100644
--- a/tests/test_send_simple_code.py
+++ b/tests/test_send_simple_code.py
@@ -1,17 +1,12 @@
import unittest
-from unittest.mock import patch
import threading
import os
-import pathlib
import socket
import tempfile
-import time
-import importlib.util
import json
from src.dsf import PROTOCOL_VERSION
from src.dsf.connections import CommandConnection
-from tests.utils import check_json
class TestSendSimpleCode(unittest.TestCase):
diff --git a/tests/test_subscribe_object_model.py b/tests/test_subscribe_object_model.py
index 903aa25..9c77d86 100644
--- a/tests/test_subscribe_object_model.py
+++ b/tests/test_subscribe_object_model.py
@@ -1,14 +1,11 @@
import unittest
-from unittest.mock import patch
+import unittest.mock
import threading
import os
-import pathlib
import socket
import tempfile
import time
-import importlib.util
import json
-from typing import Union, Dict
from tests.utils import check_json
from src.dsf import PROTOCOL_VERSION
From 14811e28d67ab12280af74ac1a18c671a4b2a419 Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:24:25 +0100
Subject: [PATCH 03/19] chore: remove python-dateutil dependency
---
.github/workflows/build-deb-package.yml | 1 -
requirements.txt | 1 -
setup.py | 3 ---
3 files changed, 5 deletions(-)
diff --git a/.github/workflows/build-deb-package.yml b/.github/workflows/build-deb-package.yml
index 6c6551b..38cdd5f 100644
--- a/.github/workflows/build-deb-package.yml
+++ b/.github/workflows/build-deb-package.yml
@@ -34,7 +34,6 @@ jobs:
python3 -m pip install --upgrade pip
pip3 install setuptools
pip3 install stdeb
- pip3 install python-dateutil
pip3 install wheel
- name: Clean up previous builds
run: rm -rf deb_dist/ || true
diff --git a/requirements.txt b/requirements.txt
index dd1f3d8..d1f3ea6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,6 +3,5 @@ wheel
build
twine
bump2version
-python-dateutil
stdeb
requests
diff --git a/setup.py b/setup.py
index 123a96e..7dfdc04 100644
--- a/setup.py
+++ b/setup.py
@@ -25,9 +25,6 @@
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3 :: Only",
],
- install_requires=[
- 'python-dateutil',
- ],
keywords="Duet3D, DuetSoftwareFramework, DSF, dsf-python",
package_dir={"": "src"},
packages=setuptools.find_packages(where="src"),
From 82edbdea2f59f35a628a5715ce5f4a98df5cee9c Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:44:15 +0100
Subject: [PATCH 04/19] fix: resolve type errors reported by pyright
- Kinematics: fall back to a plain Kinematics instance for unhandled names
instead of returning the enum value
- FilamentMonitor: only switch monitor type when a string type is given
- DriverId: default board to 0 when parsing a port-only string, raise
TypeError when updating from a non-string, type __eq__/__ne__
- ObjectModel: raise TypeError when "global" is neither a dict nor null
- ProbeType: remove trailing commas that made member values tuples
- ModelCollection: accept JSON elements as initial values
---
src/dsf/object_model/model_collection.py | 4 +--
src/dsf/object_model/move/driver_id.py | 33 +++++++++----------
.../move/kinematics/kinematics.py | 19 ++++++-----
src/dsf/object_model/object_model.py | 8 +++--
.../filament_monitors/filament_monitor.py | 12 +++----
src/dsf/object_model/sensors/probe_type.py | 20 +++++------
6 files changed, 50 insertions(+), 46 deletions(-)
diff --git a/src/dsf/object_model/model_collection.py b/src/dsf/object_model/model_collection.py
index 5f851df..4b8def2 100644
--- a/src/dsf/object_model/model_collection.py
+++ b/src/dsf/object_model/model_collection.py
@@ -1,5 +1,5 @@
from enum import Enum
-from typing import Callable, Generic, TypeVar, List, Union, Optional, get_origin, get_args, cast
+from typing import Callable, Generic, Sequence, TypeVar, Union, Optional, get_origin, get_args, cast
from ..utils import JSONElement
from .model_type import ModelType
@@ -13,7 +13,7 @@ class ModelCollection(ModelType[list[JSONElement]], Generic[T], list[T]):
Useful for updating model object items from JSON data (patches)
"""
- def __init__(self, item_constructor: type[T] | object, value: Optional[List[T]] = None, allow_none: bool = False) -> None:
+ def __init__(self, item_constructor: type[T] | object, value: Optional[Sequence[T | JSONElement]] = None, allow_none: bool = False) -> None:
"""
:param item_constructor: Item constructor type that items must derive from
:param value: Value used to initialize the list from
diff --git a/src/dsf/object_model/move/driver_id.py b/src/dsf/object_model/move/driver_id.py
index c9d4332..a2471b6 100644
--- a/src/dsf/object_model/move/driver_id.py
+++ b/src/dsf/object_model/move/driver_id.py
@@ -1,9 +1,10 @@
import re
-from typing import Optional
+from typing import Optional, Self
from ..model_object import ModelObject
from ...exceptions import CodeParserException
+from ...utils import JSONObj
def is_driverId(value: object) -> bool:
@@ -53,26 +54,24 @@ def as_int(self) -> int:
def __str__(self, **kwargs: object):
"""Convert this instance to a string"""
- return f"{self.port}" if self.board is None else f"{self.board}.{self.port}"
+ return f"{self.board}.{self.port}"
- def __eq__(self, o):
+ def __eq__(self, o: object) -> bool:
"""Checks whether this instance is equal to another"""
- if self is None:
- return o is None
return isinstance(o, DriverId) and self.board == o.board and self.port == o.port
- def __ne__(self, o):
+ def __ne__(self, o: object) -> bool:
return not self == o
- def update_from_json(self, data):
- if isinstance(data, str):
- matches = re.search(r'(\d+)\.(\d+)', data)
- if matches:
- self.board = int(matches.group(1))
- self.port = int(matches.group(2))
- else:
- self.board = None
- self.port = int(data)
- return self
- return None
+ def update_from_json(self, data: JSONObj | str) -> Self:
+ if not isinstance(data, str):
+ raise TypeError(f"DriverId must be updated from a string. Got {type(data).__name__}: {data}")
+ matches = re.search(r'(\d+)\.(\d+)', data)
+ if matches:
+ self.board = int(matches.group(1))
+ self.port = int(matches.group(2))
+ else:
+ self.board = 0
+ self.port = int(data)
+ return self
diff --git a/src/dsf/object_model/move/kinematics/kinematics.py b/src/dsf/object_model/move/kinematics/kinematics.py
index ce56721..fc1f723 100644
--- a/src/dsf/object_model/move/kinematics/kinematics.py
+++ b/src/dsf/object_model/move/kinematics/kinematics.py
@@ -2,6 +2,7 @@
from ..move_segmentation import MoveSegmentation
from ...model_object import ModelObject
from ...utils import model_prop, nullable_model_prop
+from ....utils import JSONElement
class Kinematics(ModelObject):
@@ -16,7 +17,7 @@ def __init__(self, name: KinematicsName = KinematicsName.unknown):
self._segmentation = None
@staticmethod
- def get_kinematics_type(name: KinematicsName | str):
+ def get_kinematics_type(name: KinematicsName | str) -> "Kinematics":
from .core_kinematics import CoreKinematics
from .delta_kinematics import DeltaKinematics
from .hangprinter_kinematics import HangprinterKinematics
@@ -51,17 +52,17 @@ def get_kinematics_type(name: KinematicsName | str):
return ScaraKinematics(name)
elif name == KinematicsName.polar:
return PolarKinematics()
- return name
+ return Kinematics(name)
- def _update_from_json(self, **kwargs):
+ def _update_from_json(self, **kwargs: JSONElement) -> "Kinematics":
"""Override ObjectModel._update_from_json to return the Kinematics type matching the given name"""
- if 'name' in kwargs:
- kwargs['name'] = KinematicsName(kwargs.get('name').lower().replace(' ', ''))
+ name = kwargs.get('name')
+ if isinstance(name, str):
+ kinematics_name = KinematicsName(name.lower().replace(' ', ''))
+ kwargs['name'] = kinematics_name
- if self.name != kwargs.get('name'):
- kinematic_type = self.get_kinematics_type(kwargs.get('name'))
- new_kinematic = kinematic_type.update_from_json(kwargs)
- return new_kinematic
+ if self.name != kinematics_name:
+ return self.get_kinematics_type(kinematics_name).update_from_json(kwargs)
super(Kinematics, self)._update_from_json(**kwargs)
return self
diff --git a/src/dsf/object_model/object_model.py b/src/dsf/object_model/object_model.py
index 2946d71..a3751db 100644
--- a/src/dsf/object_model/object_model.py
+++ b/src/dsf/object_model/object_model.py
@@ -3,6 +3,7 @@
from .model_collection import ModelCollection
from .model_dictionary import ModelDictionary
from .model_object import ModelObject
+from ..utils import JSONElement
from .boards import Boards
from .directories import Directories
from .fans import Fan
@@ -53,10 +54,13 @@ class ObjectModel(ModelObject):
def __init__(self):
super(ObjectModel, self).__init__()
- def _update_from_json(self, **kwargs) -> 'ObjectModel':
+ def _update_from_json(self, **kwargs: JSONElement) -> 'ObjectModel':
super(ObjectModel, self)._update_from_json(**kwargs)
# "global" is a reserved keyword in Python, so it is converted to "globals"
if 'global_' in kwargs:
- self.globals.update_from_json(kwargs.get('global_'))
+ global_ = kwargs['global_']
+ if global_ is not None and not isinstance(global_, dict):
+ raise TypeError(f"global must be a dict or None. Got {type(global_).__name__}: {global_}")
+ self.globals.update_from_json(global_)
return self
diff --git a/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py b/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py
index a20a381..90e8540 100644
--- a/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py
+++ b/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py
@@ -3,6 +3,7 @@
from .filament_monitor_type import FilamentMonitorType
from ...model_object import ModelObject
from ...utils import model_prop, nullable_model_prop
+from ....utils import JSONElement
class FilamentMonitor(ModelObject):
@@ -29,7 +30,7 @@ def __init__(self, type_: FilamentMonitorType = FilamentMonitorType.Unknown):
self.type = type_
@staticmethod
- def get_filament_monitor(type_: FilamentMonitorType):
+ def get_filament_monitor(type_: FilamentMonitorType | str) -> "FilamentMonitor":
from .laser_filament_monitor import LaserFilamentMonitor
from .pulsed_filament_monitor import PulsedFilamentMonitor
from .rotating_magnet_filament_monitor import RotatingMagnetFilamentMonitor
@@ -48,12 +49,11 @@ def get_filament_monitor(type_: FilamentMonitorType):
else:
return FilamentMonitor(type_)
- def _update_from_json(self, **kwargs):
+ def _update_from_json(self, **kwargs: JSONElement) -> "FilamentMonitor":
"""Override ObjectModel._update_from_json to return the FilamentMonitorType type matching the given type"""
- if 'type_' in kwargs and self.type != FilamentMonitorType(kwargs.get('type_')):
- required_type = self.get_filament_monitor(kwargs.get('type_'))
- new_filament_monitor = required_type.update_from_json(kwargs)
- return new_filament_monitor
+ type_ = kwargs.get('type_')
+ if isinstance(type_, str) and self.type != FilamentMonitorType(type_):
+ return self.get_filament_monitor(type_).update_from_json(kwargs)
super(FilamentMonitor, self)._update_from_json(**kwargs)
return self
diff --git a/src/dsf/object_model/sensors/probe_type.py b/src/dsf/object_model/sensors/probe_type.py
index 66fb589..bebb274 100644
--- a/src/dsf/object_model/sensors/probe_type.py
+++ b/src/dsf/object_model/sensors/probe_type.py
@@ -5,34 +5,34 @@ class ProbeType(IntEnum):
"""Supported probe types"""
# No probe
- NoProbe = 0,
+ NoProbe = 0
# A simple unmodulated probe (like dc42's infrared probe)
- Analog = 1,
+ Analog = 1
# A modulated probe (like the original one shipped with the RepRapPro Ormerod)
- DumbModulated = 2,
+ DumbModulated = 2
# Alternate analog probe (like the ultrasonic probe)
- AlternateAnalog = 3,
+ AlternateAnalog = 3
# Endstop switch (obsolete, should not be used anymore)
- EndstopSwitch_Obsolete = 4,
+ EndstopSwitch_Obsolete = 4
# A switch that is triggered when the probe is activated (filtered)
- Digital = 5,
+ Digital = 5
# Endstop switch on the E1 endstop pin (obsolete, should not be used anymore)
- E1Switch_Obsolete = 6,
+ E1Switch_Obsolete = 6
# Endstop switch on Z endstop pin (obsolete, should not be used anymore)
- ZSwitch_Obsolete = 7,
+ ZSwitch_Obsolete = 7
# A switch that is triggered when the probe is activated (unfiltered)
- UnfilteredDigital = 8,
+ UnfilteredDigital = 8
# A BLTouch probe
- BLTouch = 9,
+ BLTouch = 9
# Z motor stall detection
ZMotorStall = 10
From 69e7baac4dac15db270d9405d33074f13a3b95ce Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:44:15 +0100
Subject: [PATCH 05/19] chore: switch type checking from mypy to pyright
- Add [tool.pyright] config to pyproject.toml (standard mode)
- Run pyright in tox and in the CI test workflow
- Pin pyright in requirements.txt
- Remove the VS Code typeCheckingMode override, pyproject.toml now applies
- Fix type errors in tests by narrowing optional and subclass values
---
.github/workflows/python-test.yml | 3 +
.vscode/settings.json | 3 +-
pyproject.toml | 7 ++
requirements.txt | 1 +
tests/test_base_connection.py | 7 +-
tests/test_object_model.py | 99 ++++++++++++++++++----------
tests/test_subscribe_object_model.py | 1 +
tests/utils.py | 3 +-
tox.ini | 4 +-
9 files changed, 84 insertions(+), 44 deletions(-)
diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml
index caf4ce6..72957fc 100644
--- a/.github/workflows/python-test.yml
+++ b/.github/workflows/python-test.yml
@@ -37,6 +37,9 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+ - name: Type check with pyright
+ run: |
+ pyright
- name: Test with pytest
run: |
pytest
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 6b9625a..def1bd8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,6 +8,5 @@
"-p",
"test_*.py"
],
- "debugpy.debugJustMyCode": false,
- "python.analysis.typeCheckingMode": "strict"
+ "debugpy.debugJustMyCode": false
}
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 374b58c..3f55da9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,3 +4,10 @@ requires = [
"wheel"
]
build-backend = "setuptools.build_meta"
+
+[tool.pyright]
+include = ["src", "tests", "examples"]
+# Examples import the package as `dsf`, tests import it as `src.dsf`
+extraPaths = ["src"]
+pythonVersion = "3.11"
+typeCheckingMode = "standard"
diff --git a/requirements.txt b/requirements.txt
index d1f3ea6..52a8fb1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,3 +5,4 @@ twine
bump2version
stdeb
requests
+pyright[nodejs]==1.1.414
diff --git a/tests/test_base_connection.py b/tests/test_base_connection.py
index 1b21609..160bc56 100644
--- a/tests/test_base_connection.py
+++ b/tests/test_base_connection.py
@@ -1,5 +1,6 @@
+import socket
import unittest
-from unittest.mock import patch
+from unittest.mock import Mock, patch
from src.dsf.connections.base_connection import BaseConnection
@@ -14,14 +15,14 @@ def test_has_data_available_returns_true_for_complete_buffered_json(self):
def test_has_data_available_returns_false_for_partial_buffer_without_socket_data(self):
connection = BaseConnection()
connection.input = '{"key"'
- connection.socket = object()
+ connection.socket = Mock(spec=socket.socket)
with patch('src.dsf.connections.base_connection.select.select', return_value=([], [], [])):
self.assertFalse(connection.has_data_available())
def test_has_data_available_returns_true_when_socket_is_readable(self):
connection = BaseConnection()
- connection.socket = object()
+ connection.socket = Mock(spec=socket.socket)
with patch(
'src.dsf.connections.base_connection.select.select',
diff --git a/tests/test_object_model.py b/tests/test_object_model.py
index 94a2e8f..f028a05 100644
--- a/tests/test_object_model.py
+++ b/tests/test_object_model.py
@@ -116,7 +116,7 @@ def test_update_from_json_model_object(self):
model.update_from_json(patch)
self.assertEqual(model.p_model.value, 10)
- self.assertIsNotNone(model.np_model)
+ assert model.np_model is not None
self.assertEqual(model.np_model.value, 20)
model.update_from_json({'np_model': None})
@@ -138,11 +138,12 @@ def test_update_from_json_model_collection(self):
}
model.update_from_json(patch)
+ assert model.np_model_collection is not None
+ assert model.np_model_ncollection is not None
self.assertEqual(len(model.p_model_collection), 1)
self.assertIsNotNone(model.p_model_ncollection)
self.assertEqual(len(model.p_model_ncollection), 2)
self.assertEqual(len(model.np_model_collection), 1)
- self.assertIsNotNone(model.np_model_ncollection)
self.assertEqual(len(model.np_model_ncollection), 2)
self.assertIsNotNone(model.p_model_collection[0])
@@ -159,13 +160,17 @@ def test_update_from_json_model_collection(self):
model.update_from_json({'p_model_ncollection': [{'value': 30}, None, {'value': 40}], 'np_model_ncollection': [{'value': 50}, None, {'value': 60}]})
self.assertEqual(len(model.p_model_ncollection), 3)
- self.assertEqual(model.p_model_ncollection[0].value, 30)
- self.assertIsNone(model.p_model_ncollection[1])
- self.assertEqual(model.p_model_ncollection[2].value, 40)
+ p_first, p_second, p_third = model.p_model_ncollection
+ assert p_first is not None and p_third is not None
+ self.assertEqual(p_first.value, 30)
+ self.assertIsNone(p_second)
+ self.assertEqual(p_third.value, 40)
self.assertEqual(len(model.np_model_ncollection), 3)
- self.assertEqual(model.np_model_ncollection[0].value, 50)
- self.assertIsNone(model.np_model_ncollection[1])
- self.assertEqual(model.np_model_ncollection[2].value, 60)
+ np_first, np_second, np_third = model.np_model_ncollection
+ assert np_first is not None and np_third is not None
+ self.assertEqual(np_first.value, 50)
+ self.assertIsNone(np_second)
+ self.assertEqual(np_third.value, 60)
model.update_from_json({'p_model_ncollection': [None], 'np_model_ncollection': [None]})
self.assertEqual(len(model.p_model_ncollection), 1)
@@ -257,8 +262,10 @@ def test_nullable_model_object_list(self):
self.assertTrue(is_model_object(model[0]))
self.assertIsNone(model[1])
self.assertTrue(is_model_object(model[2]))
- self.assertEqual(model[0].current, 10)
- self.assertEqual(model[2].current, 20)
+ first, _, third = model
+ assert first is not None and third is not None
+ self.assertEqual(first.current, 10)
+ self.assertEqual(third.current, 20)
patch = [None, {}]
model.update_from_json(patch)
@@ -346,6 +353,7 @@ def test_instance_defaults_are_isolated(self):
m1.update_from_json('{"sbc": {}}')
m2.update_from_json('{"sbc": {}}')
+ assert m1.sbc is not None and m2.sbc is not None
self.assertIsNot(m1.sbc.dsf.user_sessions, m2.sbc.dsf.user_sessions)
def test_boards(self):
@@ -358,6 +366,7 @@ def test_boards(self):
json_patch = '{"boards":[{"vIn":{"current":42.5}}]}'
model.update_from_json(json_patch)
# Check if the value has been modified
+ assert model.boards[0].v_in is not None
self.assertEqual(model.boards[0].v_in.current, 42.5)
# Check if other values has not been altered
self.assertEqual(model.boards[0].v_in.min, 19.3)
@@ -368,17 +377,19 @@ def test_boards_main_and_expansion(self):
model.update_from_json('{"boards": [{"name": "Duet 3 MB6HC", "firmwareName": "RepRapFirmware", "maxHeaters": 32},'
' {"canAddress": 1, "name": "Duet 3 EXP3HC", "state": "timedOut", "timeout": 15}]}')
self.assertIsInstance(model.boards, Boards)
- self.assertIsInstance(model.boards[0], MainBoard)
- self.assertIsInstance(model.boards[1], ExpansionBoard)
- self.assertEqual(model.boards[0].firmware_name, "RepRapFirmware")
- self.assertEqual(model.boards[0].max_heaters, 32)
- self.assertEqual(model.boards[1].state, BoardState.timedOut)
- self.assertEqual(model.boards[1].timeout, 15)
+ main_board, expansion_board = model.boards
+ assert isinstance(main_board, MainBoard)
+ assert isinstance(expansion_board, ExpansionBoard)
+ self.assertEqual(main_board.firmware_name, "RepRapFirmware")
+ self.assertEqual(main_board.max_heaters, 32)
+ self.assertEqual(expansion_board.state, BoardState.timedOut)
+ self.assertEqual(expansion_board.timeout, 15)
# Boards added by a later patch are typed by their position as well
model.update_from_json('{"boards": [{}, {}, {"canAddress": 2}]}')
- self.assertIsInstance(model.boards[2], ExpansionBoard)
- self.assertEqual(model.boards[2].timeout, 10)
+ new_board = model.boards[2]
+ assert isinstance(new_board, ExpansionBoard)
+ self.assertEqual(new_board.timeout, 10)
def test_sensors_accelerometers_and_load_cell(self):
model = ObjectModel()
@@ -388,14 +399,15 @@ def test_sensors_accelerometers_and_load_cell(self):
' "preload": 50, "preloadWindow": [10, 100]}}]}}')
self.assertIsNone(model.sensors.accelerometers[0])
accelerometer = model.sensors.accelerometers[1]
- self.assertIsInstance(accelerometer, Accelerometer)
+ assert isinstance(accelerometer, Accelerometer)
self.assertEqual(accelerometer.port, "121.spi.cs0")
self.assertEqual(accelerometer.resolution, 16)
self.assertEqual(accelerometer.sampling_rate, 1344)
probe = model.sensors.probes[0]
+ assert probe is not None
self.assertEqual(probe.type, ProbeType.LoadCell)
- self.assertIsInstance(probe.load_cell, ProbeLoadCell)
+ assert isinstance(probe.load_cell, ProbeLoadCell)
self.assertEqual(probe.load_cell.force, 12.5)
self.assertEqual(list(probe.load_cell.preload_window), [10.0, 100.0])
@@ -417,7 +429,9 @@ def test_rc2_fields(self):
self.assertEqual(model.move.shaping.type, InputShapingType.ei2)
self.assertEqual(model.move.motion_systems[0].printing_acceleration, 3000)
self.assertEqual(list(model.move.motion_systems[0].user_position), [1.0, 2.0, 3.0])
+ assert model.job.build is not None
self.assertTrue(model.job.build.objects[0].cancelled)
+ assert model.sbc is not None and model.sbc.upgrade is not None
self.assertEqual(model.sbc.upgrade.message, "Installing packages")
self.assertEqual(model.sbc.upgrade.progress, 0.5)
@@ -488,6 +502,7 @@ def test_reserved_keys(self):
self.assertEqual(model.move.shaping.type, InputShapingType.ei2)
self.assertEqual(model.plugins['TestPlugin'].id, "TestPlugin")
self.assertEqual(model.plugins['TestPlugin'].license, "MIT")
+ assert model.state.message_box is not None
self.assertEqual(model.state.message_box.max, 10.5)
self.assertEqual(model.state.message_box.min, -1.5)
@@ -510,6 +525,7 @@ def test_http_endpoints(self):
json_patch = '{"sbc":{"dsf":{"httpEndpoints":[{"endpointType":"GET","namespace":"ExecOnMcode","path":"getCmdList","isUploadRequest":false,"unixSocket":"/run/dsf/ExecOnMcode/getCmdList-GET.sock"}]}}}'
model.update_from_json(json_patch)
+ assert model.sbc is not None
self.assertEqual(len(model.sbc.dsf.http_endpoints), 1)
self.assertEqual(model.sbc.dsf.http_endpoints[0].endpoint_type, HttpEndpointType.GET)
@@ -530,26 +546,28 @@ def test_inputs(self):
model.update_from_json(json_patch)
self.assertEqual(len(model.inputs), 4)
- self.assertIsInstance(model.inputs[0], InputChannel)
- self.assertIsNone(model.inputs[1])
- self.assertIsInstance(model.inputs[2], InputChannel)
- self.assertIsNone(model.inputs[3])
- self.assertEqual(model.inputs[0].name, CodeChannel.HTTP)
- self.assertEqual(model.inputs[2].name, CodeChannel.File)
- self.assertEqual(model.inputs[2].line_number, 42)
+ first_channel, second_channel, third_channel, fourth_channel = model.inputs
+ assert isinstance(first_channel, InputChannel)
+ self.assertIsNone(second_channel)
+ assert isinstance(third_channel, InputChannel)
+ self.assertIsNone(fourth_channel)
+ self.assertEqual(first_channel.name, CodeChannel.HTTP)
+ self.assertEqual(third_channel.name, CodeChannel.File)
+ self.assertEqual(third_channel.line_number, 42)
# Existing channels are updated in place, not replaced
- first_channel = model.inputs[0]
model.update_from_json('{"inputs":[{"state":"executing"},null,{"lineNumber":43},null]}')
self.assertIs(model.inputs[0], first_channel)
- self.assertEqual(model.inputs[0].state, InputChannelState.executing)
- self.assertEqual(model.inputs[2].line_number, 43)
+ self.assertIs(model.inputs[2], third_channel)
+ self.assertEqual(first_channel.state, InputChannelState.executing)
+ self.assertEqual(third_channel.line_number, 43)
# A channel may become null, and a previously null one may become a channel
model.update_from_json('{"inputs":[null,{"name":"Telnet"},null,null]}')
self.assertIsNone(model.inputs[0])
- self.assertIsInstance(model.inputs[1], InputChannel)
- self.assertEqual(model.inputs[1].name, CodeChannel.Telnet)
+ second_channel = model.inputs[1]
+ assert isinstance(second_channel, InputChannel)
+ self.assertEqual(second_channel.name, CodeChannel.Telnet)
self.assertIsNone(model.inputs[2])
# Helper properties still work
@@ -608,7 +626,7 @@ def test_move_kinematics(self):
json_patch = '{"move": {"kinematics": {"name": "delta","deltaRadius": 123}}}'
model.update_from_json(json_patch)
- self.assertIsInstance(model.move.kinematics, DeltaKinematics)
+ assert isinstance(model.move.kinematics, DeltaKinematics)
self.assertEqual(model.move.kinematics.name, KinematicsName.linearDelta)
self.assertEqual(model.move.kinematics.delta_radius, 123)
@@ -621,7 +639,7 @@ def test_move_kinematics(self):
# Switch to linear delta (eg: M669 K3)
json_patch = '{"move":{"kinematics":{"deltaRadius":105.6,"homedHeight":240,"printRadius":80,"towers":[{"angleCorrection":0,"diagonal":215,"endstopAdjustment":0,"xPos":-91.452,"yPos":-52.8},{"angleCorrection":0,"diagonal":215,"endstopAdjustment":0,"xPos":91.452,"yPos":-52.8},{"angleCorrection":0,"diagonal":215,"endstopAdjustment":0,"xPos":0,"yPos":105.6}],"xTilt":0,"yTilt":0,"name":"delta","segmentation":null}}}'
model.update_from_json(json_patch)
- self.assertIsInstance(model.move.kinematics, DeltaKinematics)
+ assert isinstance(model.move.kinematics, DeltaKinematics)
self.assertEqual(model.move.kinematics.name, KinematicsName.linearDelta)
self.assertEqual(model.move.kinematics.delta_radius, 105.6)
@@ -658,21 +676,29 @@ def test_sensors_filament_monitor(self):
json_patch = '{"sensors":{"filamentMonitors":[{"enabled":true,"status":"ok","type":"simple"}]}}'
model.update_from_json(json_patch)
self.assertEqual(len(model.sensors.filament_monitors), 1)
- self.assertEqual(model.sensors.filament_monitors[0].type, FilamentMonitorType.Simple)
+ filament_monitor = model.sensors.filament_monitors[0]
+ assert filament_monitor is not None
+ self.assertEqual(filament_monitor.type, FilamentMonitorType.Simple)
# Change filament monitor to Pulsed (rg: M591 D0 P7 C"io2.in" S1)
json_patch = '{"sensors":{"filamentMonitors":[{"calibrated":null,"configured":{"mmPerPulse":1,"percentMax":160,"percentMin":60,"sampleDistance":5},"enabled":true,"status":"ok","type":"pulsed"}]}}'
model.update_from_json(json_patch)
self.assertEqual(len(model.sensors.filament_monitors), 1)
- self.assertEqual(model.sensors.filament_monitors[0].type, FilamentMonitorType.Pulsed)
+ filament_monitor = model.sensors.filament_monitors[0]
+ assert filament_monitor is not None
+ self.assertEqual(filament_monitor.type, FilamentMonitorType.Pulsed)
def test_sensors_filament_monitor_rc2_fields(self):
+ from src.dsf.object_model.sensors.filament_monitors import RotatingMagnetFilamentMonitor
+
model = ObjectModel()
model.update_from_json('{"sensors":{"filamentMonitors":[{"type":"rotatingMagnet","filamentPresent":true,"agc":120,'
'"calibrated":{"mmPerRev":28.8,"percentMax":110,"percentMin":90,"totalDistance":100}}]}}')
monitor = model.sensors.filament_monitors[0]
+ assert isinstance(monitor, RotatingMagnetFilamentMonitor)
self.assertTrue(monitor.filament_present)
self.assertEqual(monitor.agc, 120)
+ assert monitor.calibrated is not None
self.assertEqual(monitor.calibrated.mm_per_rev, 28.8)
def test_user_sessions(self):
@@ -682,6 +708,7 @@ def test_user_sessions(self):
json_patch = '{"sbc": {}}'
model.update_from_json(json_patch)
+ assert model.sbc is not None
self.assertEqual(len(model.sbc.dsf.user_sessions), 0)
json_patch = '{"sbc":{"dsf":{"userSessions":[{"accessLevel":"readWrite","id":2,"origin":"::ffff:192.168.1.200","originId":-1,"sessionType":"http"}]}}}'
diff --git a/tests/test_subscribe_object_model.py b/tests/test_subscribe_object_model.py
index 9c77d86..d591b75 100644
--- a/tests/test_subscribe_object_model.py
+++ b/tests/test_subscribe_object_model.py
@@ -201,6 +201,7 @@ def test_subscribe_to_keys_runs_callback_for_matching_changes(self):
self.assertIn(("state.upTime", 3658, None), callback_changes)
self.assertIn(("boards", unittest.mock.ANY, None), callback_changes)
boards_data = next(data for key, data, indices in callback_changes if key == "boards")
+ assert isinstance(boards_data, list)
self.assertEqual(len(boards_data), 7)
unsubscribe()
diff --git a/tests/utils.py b/tests/utils.py
index 2ebc26f..561396d 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,7 +1,8 @@
import json
+from typing import Mapping
-def check_json(expected_dict: dict[str, object], json_str: str) -> None:
+def check_json(expected_dict: Mapping[str, object], json_str: str) -> None:
try:
json_obj = json.loads(json_str)
except json.JSONDecodeError:
diff --git a/tox.ini b/tox.ini
index 55b590a..849eb29 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,13 +5,13 @@ envlist = py{36,37,38,39}
deps =
check-manifest
black
- mypy
+ pyright[nodejs]==1.1.414
flake8
pytest
commands =
check-manifest --ignore 'tox.ini,docs/**,examples/**,tests/**'
python setup.py check -m -s
black --check --line-length 120 .
- mypy src
+ pyright
flake8 src examples tests
pytest
From f17e9fa5e39a357b334cb248831bc02ef53e233a Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:48:13 +0100
Subject: [PATCH 06/19] chore: add `__all__` to `__init__.py`
---
pyproject.toml | 2 +-
src/dsf/__init__.py | 3 +++
src/dsf/commands/__init__.py | 3 +++
src/dsf/connections/__init__.py | 3 +++
src/dsf/connections/init_messages/__init__.py | 3 +++
src/dsf/object_model/__init__.py | 3 +++
src/dsf/object_model/led_strips/__init__.py | 3 +++
src/dsf/object_model/move/__init__.py | 3 +++
src/dsf/object_model/plugins/__init__.py | 3 +++
src/dsf/object_model/sbc/__init__.py | 3 +++
src/dsf/object_model/sbc/dsf/__init__.py | 3 +++
src/dsf/object_model/sensors/__init__.py | 3 +++
src/dsf/object_model/sensors/filament_monitors/__init__.py | 3 +++
src/dsf/object_model/state/__init__.py | 3 +++
14 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 3f55da9..9a9f2ab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,4 +10,4 @@ include = ["src", "tests", "examples"]
# Examples import the package as `dsf`, tests import it as `src.dsf`
extraPaths = ["src"]
pythonVersion = "3.11"
-typeCheckingMode = "standard"
+typeCheckingMode = "strict"
diff --git a/src/dsf/__init__.py b/src/dsf/__init__.py
index a6234ba..0c8af83 100644
--- a/src/dsf/__init__.py
+++ b/src/dsf/__init__.py
@@ -25,3 +25,6 @@
PROTOCOL_VERSION = 13
from . import commands, connections, http, object_model
+
+
+__all__ = ['SOCKET_FILE', 'DEFAULT_BACKLOG', 'PROTOCOL_VERSION', 'commands', 'connections', 'http', 'object_model']
diff --git a/src/dsf/commands/__init__.py b/src/dsf/commands/__init__.py
index 66b8544..01d16e2 100644
--- a/src/dsf/commands/__init__.py
+++ b/src/dsf/commands/__init__.py
@@ -1,2 +1,5 @@
from . import base_command, code, code_interception, code_channel, code_parameter, files, generic,\
http_endpoints, model_subscription, object_model, packages, plugins, responses, user_sessions
+
+
+__all__ = ['base_command', 'code', 'code_interception', 'code_channel', 'code_parameter', 'files', 'generic', 'http_endpoints', 'model_subscription', 'object_model', 'packages', 'plugins', 'responses', 'user_sessions']
diff --git a/src/dsf/connections/__init__.py b/src/dsf/connections/__init__.py
index ac5ece3..027cc88 100644
--- a/src/dsf/connections/__init__.py
+++ b/src/dsf/connections/__init__.py
@@ -50,3 +50,6 @@ class SubscriptionMode(str, Enum):
from .exceptions import InternalServerException, TaskCanceledException
from .intercept_connection import InterceptConnection
from .subscribe_connection import SubscribeConnection
+
+
+__all__ = ['ConnectionMode', 'InterceptionMode', 'SubscriptionMode', 'BaseCommandConnection', 'BaseConnection', 'CommandConnection', 'InternalServerException', 'TaskCanceledException', 'InterceptConnection', 'SubscribeConnection']
diff --git a/src/dsf/connections/init_messages/__init__.py b/src/dsf/connections/init_messages/__init__.py
index 4c35560..610b490 100644
--- a/src/dsf/connections/init_messages/__init__.py
+++ b/src/dsf/connections/init_messages/__init__.py
@@ -1 +1,4 @@
from . import client_init_messages, server_init_message
+
+
+__all__ = ['client_init_messages', 'server_init_message']
diff --git a/src/dsf/object_model/__init__.py b/src/dsf/object_model/__init__.py
index 8540a77..1111aa4 100644
--- a/src/dsf/object_model/__init__.py
+++ b/src/dsf/object_model/__init__.py
@@ -18,3 +18,6 @@
from .state import LogLevel, MachineStatus, MessageBox, MessageBoxMode, State
from .tools import Tool, ToolState
from .volumes import Volume
+
+
+__all__ = ['Board', 'Boards', 'BoardState', 'ExpansionBoard', 'MainBoard', 'Directories', 'Fan', 'Heat', 'Heater', 'HeaterState', 'InputChannel', 'Job', 'LedStrip', 'LedStripType', 'Limits', 'Message', 'MessageType', 'DriverId', 'Move', 'Network', 'NetworkInterface', 'NetworkInterfaceType', 'NetworkProtocol', 'NetworkState', 'ObjectModel', 'Plugin', 'PluginManifest', 'SbcPermissions', 'CPU', 'Memory', 'SBC', 'Upgrade', 'AccessLevel', 'HttpEndpoint', 'HttpEndpointType', 'SessionType', 'UserSession', 'Accelerometer', 'AnalogSensor', 'AnalogSensorType', 'Endstop', 'EndstopType', 'GpInputPort', 'Probe', 'ProbeLoadCell', 'ProbeType', 'Sensors', 'Spindle', 'SpindleState', 'LogLevel', 'MachineStatus', 'MessageBox', 'MessageBoxMode', 'State', 'Tool', 'ToolState', 'Volume']
diff --git a/src/dsf/object_model/led_strips/__init__.py b/src/dsf/object_model/led_strips/__init__.py
index afc7f3f..4395e7d 100644
--- a/src/dsf/object_model/led_strips/__init__.py
+++ b/src/dsf/object_model/led_strips/__init__.py
@@ -1 +1,4 @@
from .led_strip import LedStrip, LedStripType
+
+
+__all__ = ['LedStrip', 'LedStripType']
diff --git a/src/dsf/object_model/move/__init__.py b/src/dsf/object_model/move/__init__.py
index 11acb2d..cfbb647 100644
--- a/src/dsf/object_model/move/__init__.py
+++ b/src/dsf/object_model/move/__init__.py
@@ -15,3 +15,6 @@
from .move_segmentation import MoveSegmentation
from .probe_grid import ProbeGrid
from .skew import Skew
+
+
+__all__ = ['Axis', 'AxisLetter', 'CurrentMove', 'DriverId', 'Extruder', 'ExtruderNonlinear', 'InputShaping', 'InputShapingType', 'MicroStepping', 'MotorsIdleControl', 'Move', 'MoveCalibration', 'MoveCompensation', 'MoveCompensationType', 'MoveDeviations', 'MoveQueueItem', 'MoveRotation', 'MoveSegmentation', 'ProbeGrid', 'Skew']
diff --git a/src/dsf/object_model/plugins/__init__.py b/src/dsf/object_model/plugins/__init__.py
index de6e18d..0c8cc68 100644
--- a/src/dsf/object_model/plugins/__init__.py
+++ b/src/dsf/object_model/plugins/__init__.py
@@ -1,3 +1,6 @@
from .plugins import Plugin
from .plugin_manifest import PluginManifest
from .sbc_permissions import SbcPermissions
+
+
+__all__ = ['Plugin', 'PluginManifest', 'SbcPermissions']
diff --git a/src/dsf/object_model/sbc/__init__.py b/src/dsf/object_model/sbc/__init__.py
index 9173880..0c2be11 100644
--- a/src/dsf/object_model/sbc/__init__.py
+++ b/src/dsf/object_model/sbc/__init__.py
@@ -2,3 +2,6 @@
from .memory import Memory
from .sbc import SBC
from .upgrade import Upgrade
+
+
+__all__ = ['CPU', 'Memory', 'SBC', 'Upgrade']
diff --git a/src/dsf/object_model/sbc/dsf/__init__.py b/src/dsf/object_model/sbc/dsf/__init__.py
index 69d777f..243bd66 100644
--- a/src/dsf/object_model/sbc/dsf/__init__.py
+++ b/src/dsf/object_model/sbc/dsf/__init__.py
@@ -1,3 +1,6 @@
from .dsf import DSF
from .http_endpoint import HttpEndpoint, HttpEndpointType
from .user_sessions import AccessLevel, SessionType, UserSession
+
+
+__all__ = ['DSF', 'HttpEndpoint', 'HttpEndpointType', 'AccessLevel', 'SessionType', 'UserSession']
diff --git a/src/dsf/object_model/sensors/__init__.py b/src/dsf/object_model/sensors/__init__.py
index e1d7dae..6fd1677 100644
--- a/src/dsf/object_model/sensors/__init__.py
+++ b/src/dsf/object_model/sensors/__init__.py
@@ -8,3 +8,6 @@
from .probe_load_cell import ProbeLoadCell
from .probe_type import ProbeType
from .sensors import Sensors
+
+
+__all__ = ['Accelerometer', 'AnalogSensor', 'AnalogSensorType', 'Endstop', 'EndstopType', 'GpInputPort', 'Probe', 'ProbeLoadCell', 'ProbeType', 'Sensors']
diff --git a/src/dsf/object_model/sensors/filament_monitors/__init__.py b/src/dsf/object_model/sensors/filament_monitors/__init__.py
index 8bc5f78..921e179 100644
--- a/src/dsf/object_model/sensors/filament_monitors/__init__.py
+++ b/src/dsf/object_model/sensors/filament_monitors/__init__.py
@@ -14,3 +14,6 @@
'PulsedFilamentMonitor', 'PulsedFilamentMonitorCalibrated', 'PulsedFilamentMonitorConfigured',
'RotatingMagnetFilamentMonitor', 'RotatingMagnetFilamentMonitorCalibrated',
'RotatingMagnetFilamentMonitorConfigured']
+
+
+__all__ = ['Duet3DFilamentMonitor', 'FilamentMonitor', 'FilamentMonitorStatus', 'FilamentMonitorType', 'LaserFilamentMonitor', 'LaserFilamentMonitorCalibrated', 'LaserFilamentMonitorConfigured', 'PulsedFilamentMonitor', 'PulsedFilamentMonitorCalibrated', 'PulsedFilamentMonitorConfigured', 'RotatingMagnetFilamentMonitor', 'RotatingMagnetFilamentMonitorCalibrated', 'RotatingMagnetFilamentMonitorConfigured']
diff --git a/src/dsf/object_model/state/__init__.py b/src/dsf/object_model/state/__init__.py
index 1c4281a..97fec59 100644
--- a/src/dsf/object_model/state/__init__.py
+++ b/src/dsf/object_model/state/__init__.py
@@ -6,3 +6,6 @@
from .restore_point import RestorePoint
from .startup_error import StartupError
from .state import State
+
+
+__all__ = ['BeepRequest', 'GpOutputPort', 'LogLevel', 'MachineStatus', 'MessageBox', 'MessageBoxMode', 'RestorePoint', 'StartupError', 'State']
From 68fbd19c628ce5d3bdf8c1b3a171663915c3075f Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:53:20 +0100
Subject: [PATCH 07/19] fix: resolve pyright strict mode errors
- Resolve SOCKET_FILE once via a typed helper instead of reassigning it
- Type subscription callbacks with a KeySubscriptionCallback protocol
- Remove dead isinstance checks and an unreachable constructor fallback
- Keep the runtime list check in ModelCollection.update_from_json for
untyped nested JSON, with a targeted pyright ignore
- Fix the subscribe example callback annotation: indices may be None
- Annotate test helpers and cast untyped JSON in tests
---
examples/subscribe_object_model.py | 5 +--
src/dsf/__init__.py | 32 +++++++++++--------
src/dsf/connections/subscribe_connection.py | 22 +++++++++----
src/dsf/object_model/model_collection.py | 7 ++--
src/dsf/object_model/model_dictionary.py | 6 ++--
.../move/kinematics/kinematics.py | 5 +--
.../filament_monitors/filament_monitor.py | 5 +--
src/dsf/object_model/utils.py | 2 +-
tests/test_commands.py | 3 +-
tests/test_object_model.py | 18 ++++++-----
tests/test_subscribe_object_model.py | 15 ++++-----
11 files changed, 62 insertions(+), 58 deletions(-)
diff --git a/examples/subscribe_object_model.py b/examples/subscribe_object_model.py
index 113ed14..740becd 100755
--- a/examples/subscribe_object_model.py
+++ b/examples/subscribe_object_model.py
@@ -19,8 +19,9 @@ def on_status_changed(*, key: str, data: str, indices: Optional[tuple[int, ...]]
print("Machine status changed to", data)
-def on_heater_temperature_changed(*, key: str, data: float, indices: tuple[int, ...]) -> None:
- # indices holds the list indexes matched by each ^ wildcard in the key
+def on_heater_temperature_changed(*, key: str, data: float, indices: tuple[int, ...] | None) -> None:
+ # indices holds the list indexes matched by each ^ wildcard in the key (None if the key has no wildcard)
+ assert indices is not None
print(f"Heater {indices[0]} is now at {data}C")
diff --git a/src/dsf/__init__.py b/src/dsf/__init__.py
index 0c8af83..0ed50dc 100644
--- a/src/dsf/__init__.py
+++ b/src/dsf/__init__.py
@@ -3,20 +3,24 @@
import json
import os
-# Default socket file path
-SOCKET_FILE: str = "/run/dsf/dcs.sock"
-
-# Try to read socket file path from config
-config_path = "/opt/dsf/conf/config.json"
-if os.path.exists(config_path):
- try:
- with open(config_path, 'r') as f:
- config = json.load(f)
- socket_dir = config.get("SocketDirectory", "/run/dsf")
- socket_file = config.get("SocketFile", "dcs.sock")
- SOCKET_FILE = os.path.join(socket_dir, socket_file)
- except (json.JSONDecodeError, IOError):
- pass # Use default if config file is invalid or inaccessible
+
+def _read_socket_file() -> str:
+ """Read the socket file path from the DSF config, falling back to the default path"""
+ config_path = "/opt/dsf/conf/config.json"
+ if os.path.exists(config_path):
+ try:
+ with open(config_path, 'r') as f:
+ config = json.load(f)
+ socket_dir: str = config.get("SocketDirectory", "/run/dsf")
+ socket_file: str = config.get("SocketFile", "dcs.sock")
+ return os.path.join(socket_dir, socket_file)
+ except (json.JSONDecodeError, IOError):
+ pass # Use default if config file is invalid or inaccessible
+ return "/run/dsf/dcs.sock"
+
+
+# Socket file path
+SOCKET_FILE = _read_socket_file()
# allowed connection per unix server
DEFAULT_BACKLOG = 4
diff --git a/src/dsf/connections/subscribe_connection.py b/src/dsf/connections/subscribe_connection.py
index 6d80d9a..b6cd4ee 100644
--- a/src/dsf/connections/subscribe_connection.py
+++ b/src/dsf/connections/subscribe_connection.py
@@ -1,7 +1,7 @@
import json
from dataclasses import dataclass
from threading import Lock
-from typing import Any, Callable, Sequence, List
+from typing import Any, Callable, Protocol, Sequence, List, cast
from .base_connection import BaseConnection
from .init_messages import client_init_messages
@@ -13,10 +13,17 @@
_MISSING = object()
+class KeySubscriptionCallback(Protocol):
+ """Callback invoked for a subscribed object model key"""
+
+ def __call__(self, *, key: str, data: Any, indices: tuple[int, ...] | None) -> None:
+ ...
+
+
@dataclass(frozen=True)
class _ObjectModelCallbackSubscription:
keys: tuple[str, ...]
- callback: Callable[..., None]
+ callback: KeySubscriptionCallback
class SubscribeConnection(BaseConnection):
@@ -104,7 +111,7 @@ def get_object_model_patch(self) -> str:
def subscribe_to_keys(
self,
keys: Sequence[str],
- callback: Callable[..., None],
+ callback: KeySubscriptionCallback,
) -> Callable[[], None]:
"""
Register a callback for one or more dot-delimited object model key paths.
@@ -195,9 +202,10 @@ def _walk_key_path(
return cls._walk_key_path(current_value[part], next_parts, indexes)
if isinstance(current_value, list):
+ items = cast(list[Any], current_value)
if part == "^":
matches: list[tuple[tuple[int, ...], Any]] = []
- for index, item in enumerate(current_value):
+ for index, item in enumerate(items):
if item is None:
continue
matches.extend(cls._walk_key_path(item, next_parts, indexes + (index,)))
@@ -207,9 +215,9 @@ def _walk_key_path(
index = int(part)
except ValueError:
return []
- if index < 0 or index >= len(current_value):
+ if index < 0 or index >= len(items):
return []
- item = current_value[index]
+ item = items[index]
if item is None:
return []
return cls._walk_key_path(item, next_parts, indexes)
@@ -218,7 +226,7 @@ def _walk_key_path(
@staticmethod
def _invoke_callback(
- callback: Callable[..., None],
+ callback: KeySubscriptionCallback,
*,
key: str,
data: Any,
diff --git a/src/dsf/object_model/model_collection.py b/src/dsf/object_model/model_collection.py
index 4b8def2..2fdc41e 100644
--- a/src/dsf/object_model/model_collection.py
+++ b/src/dsf/object_model/model_collection.py
@@ -40,10 +40,8 @@ def __init__(self, item_constructor: type[T] | object, value: Optional[Sequence[
if isinstance(resolved_constructor, type):
self._item_constructor: Callable[..., T] = cast(Callable[..., T], resolved_constructor)
- elif isinstance(self._runtime_model_type, type):
- self._item_constructor = cast(Callable[..., T], self._runtime_model_type)
else:
- self._item_constructor = cast(Callable[..., T], object)
+ self._item_constructor = cast(Callable[..., T], self._runtime_model_type)
if value is not None:
self[:] = []
@@ -102,7 +100,8 @@ def update_from_json(self, data: list[JSONElement]) -> 'ModelCollection[T]':
"""
from .utils import is_model_object
- if not isinstance(data, list):
+ # Nested JSON can reach this without type checking, so validate at runtime
+ if not isinstance(data, list): # pyright: ignore[reportUnnecessaryIsInstance]
raise Exception(f"Invalid JSON element type for model collection {type(data)}.")
# Remove deleted items
diff --git a/src/dsf/object_model/model_dictionary.py b/src/dsf/object_model/model_dictionary.py
index 04001de..bf88195 100644
--- a/src/dsf/object_model/model_dictionary.py
+++ b/src/dsf/object_model/model_dictionary.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional, Self, TYPE_CHECKING
+from typing import Any, Optional, Self, TYPE_CHECKING, cast
from .model_type import ModelType
from ..utils import JSONObj
@@ -24,8 +24,6 @@ def __init__(self, null_deletes_keys: bool, item_constructor: Optional[type["Mod
self._null_deletes_keys = null_deletes_keys
if value is not None:
- if not isinstance(value, dict):
- raise TypeError(f"value must be of type dict or None. Got {type(value)}: {value}")
for k, v in value.items():
self[k] = v
@@ -43,7 +41,7 @@ def __setitem__(self, key: str, value: Any) -> None:
if not isinstance(value, dict):
raise TypeError(f"Value for key '{key}' must be of type dict to update the model object. Got {type(value)}: {value}")
new_item = self._item_constructor()
- return super().__setitem__(key, new_item.update_from_json(value))
+ return super().__setitem__(key, new_item.update_from_json(cast(JSONObj, value)))
elif is_model_object(current_item):
return super().__setitem__(key, current_item.update_from_json(value))
diff --git a/src/dsf/object_model/move/kinematics/kinematics.py b/src/dsf/object_model/move/kinematics/kinematics.py
index fc1f723..bb6639b 100644
--- a/src/dsf/object_model/move/kinematics/kinematics.py
+++ b/src/dsf/object_model/move/kinematics/kinematics.py
@@ -28,10 +28,7 @@ def get_kinematics_type(name: KinematicsName | str) -> "Kinematics":
:param name: Kinematics name
:returns: Required type
"""
- if isinstance(name, str):
- name = KinematicsName(name.lower().replace(' ', ''))
- elif not isinstance(name, KinematicsName):
- raise TypeError(f'{__name__} must be KinematicsName. Got {type(name)}: {name}')
+ name = KinematicsName(name.lower().replace(' ', ''))
if name in [
KinematicsName.cartesian,
diff --git a/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py b/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py
index 90e8540..b71f242 100644
--- a/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py
+++ b/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py
@@ -35,10 +35,7 @@ def get_filament_monitor(type_: FilamentMonitorType | str) -> "FilamentMonitor":
from .pulsed_filament_monitor import PulsedFilamentMonitor
from .rotating_magnet_filament_monitor import RotatingMagnetFilamentMonitor
- if isinstance(type_, str):
- type_ = FilamentMonitorType(type_)
- elif not isinstance(type_, FilamentMonitorType):
- raise TypeError(f"type must be of type FilamentMonitorType. Got {type(type_)}: {type_}")
+ type_ = FilamentMonitorType(type_)
if type_ == FilamentMonitorType.Laser:
return LaserFilamentMonitor()
diff --git a/src/dsf/object_model/utils.py b/src/dsf/object_model/utils.py
index c898549..481e5aa 100644
--- a/src/dsf/object_model/utils.py
+++ b/src/dsf/object_model/utils.py
@@ -82,7 +82,7 @@ def _make_default() -> T:
if _mutable_model_default:
if default is None:
return model_type()
- return cast(T, copy.deepcopy(default))
+ return copy.deepcopy(default)
return cast(T, _scalar_default)
def getter(self: object) -> T:
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 13e9c99..fc464f4 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -1,11 +1,12 @@
import json
import unittest
+from typing import Any
from src.dsf.commands import generic, object_model
from src.dsf.connections.init_messages import client_init_messages
-def serialize(obj: object) -> dict:
+def serialize(obj: object) -> dict[str, Any]:
return json.loads(json.dumps(obj, default=lambda o: o.__dict__))
diff --git a/tests/test_object_model.py b/tests/test_object_model.py
index f028a05..7564e28 100644
--- a/tests/test_object_model.py
+++ b/tests/test_object_model.py
@@ -1,7 +1,7 @@
import json
import unittest
-from typing import Optional
+from typing import Optional, cast
from src.dsf.object_model import *
from src.dsf.object_model.utils import is_model_object, JSONElement, JSONObj, model_prop, nullable_model_prop
@@ -585,20 +585,22 @@ def test_json_serialization(self):
json_data = json.load(fp)
model = ObjectModel.from_json(json_data)
- def recursive_compare(obj1, obj2):
+ def recursive_compare(obj1: object, obj2: object) -> None:
if isinstance(obj1, dict):
self.assertIsInstance(obj2, dict)
- self.assertEqual(set(obj1.keys()), set(obj2.keys()))
- for key in obj1:
- recursive_compare(obj1[key], obj2[key])
+ dict1, dict2 = cast(dict[str, object], obj1), cast(dict[str, object], obj2)
+ self.assertEqual(set(dict1.keys()), set(dict2.keys()))
+ for key in dict1:
+ recursive_compare(dict1[key], dict2[key])
elif isinstance(obj1, list):
self.assertIsInstance(obj2, list)
- self.assertEqual(len(obj1), len(obj2))
- for item1, item2 in zip(obj1, obj2):
+ list1, list2 = cast(list[object], obj1), cast(list[object], obj2)
+ self.assertEqual(len(list1), len(list2))
+ for item1, item2 in zip(list1, list2):
recursive_compare(item1, item2)
elif isinstance(obj1, ModelObject):
self.assertIsInstance(obj2, ModelObject)
- recursive_compare(obj1.__dict__, obj2.__dict__)
+ recursive_compare(vars(obj1), vars(obj2))
else:
self.assertEqual(obj1, obj2)
diff --git a/tests/test_subscribe_object_model.py b/tests/test_subscribe_object_model.py
index d591b75..606ec8a 100644
--- a/tests/test_subscribe_object_model.py
+++ b/tests/test_subscribe_object_model.py
@@ -6,6 +6,7 @@
import tempfile
import time
import json
+from typing import cast
from tests.utils import check_json
from src.dsf import PROTOCOL_VERSION
@@ -185,9 +186,7 @@ def test_subscribe_to_keys_runs_callback_for_matching_changes(self):
unsubscribe = subscribe_connection.subscribe_to_keys(
["boards", "heat.heaters.0.current", "state.upTime"],
- lambda **kwargs: callback_changes.append(
- (kwargs["key"], kwargs["data"], kwargs["indices"])
- ),
+ lambda key, data, indices: callback_changes.append((key, data, indices)),
)
self.assertTrue(
@@ -200,9 +199,9 @@ def test_subscribe_to_keys_runs_callback_for_matching_changes(self):
self.assertIn(("heat.heaters.0.current", 16.22, None), callback_changes)
self.assertIn(("state.upTime", 3658, None), callback_changes)
self.assertIn(("boards", unittest.mock.ANY, None), callback_changes)
- boards_data = next(data for key, data, indices in callback_changes if key == "boards")
- assert isinstance(boards_data, list)
- self.assertEqual(len(boards_data), 7)
+ boards_data = next(data for key, data, _ in callback_changes if key == "boards")
+ self.assertIsInstance(boards_data, list)
+ self.assertEqual(len(cast(list[object], boards_data)), 7)
unsubscribe()
finally:
@@ -223,9 +222,7 @@ def test_subscribe_to_keys_passes_wildcard_indexes(self):
unsubscribe = subscribe_connection.subscribe_to_keys(
["heat.heaters.^.current", "sensors.analog.^.lastReading"],
- lambda **kwargs: callback_changes.append(
- (kwargs["key"], kwargs["data"], kwargs["indices"])
- ),
+ lambda key, data, indices: callback_changes.append((key, data, indices)),
)
self.assertTrue(
From 488a5702ece97ee7cc149a8eb6ea6ba4b98d1e2c Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:11:02 +0100
Subject: [PATCH 08/19] build: ship py.typed marker and fix tox envlist
- Add py.typed so installed packages expose their type hints
- Include py.typed in the wheel (package_data) and sdist (MANIFEST.in)
- Update tox envlist to the supported Python versions (3.11-3.14)
---
MANIFEST.in | 1 +
setup.py | 1 +
src/dsf/py.typed | 0
tox.ini | 2 +-
4 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 src/dsf/py.typed
diff --git a/MANIFEST.in b/MANIFEST.in
index e6210e4..1bcf795 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,5 @@
include LICENSE
include README.md
include setup.py
+include src/dsf/py.typed
reverse-include examples/*.py
diff --git a/setup.py b/setup.py
index 7dfdc04..853e4f3 100644
--- a/setup.py
+++ b/setup.py
@@ -28,6 +28,7 @@
keywords="Duet3D, DuetSoftwareFramework, DSF, dsf-python",
package_dir={"": "src"},
packages=setuptools.find_packages(where="src"),
+ package_data={"dsf": ["py.typed"]},
python_requires=">=3.11, <4",
extras_require={
"dev": [
diff --git a/src/dsf/py.typed b/src/dsf/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/tox.ini b/tox.ini
index 849eb29..47f618f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py{36,37,38,39}
+envlist = py{311,312,313,314}
[testenv]
deps =
From 026dd8fe7896b2f47fb43f28919e59a36d27484d Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:11:02 +0100
Subject: [PATCH 09/19] fix: raise ConnectionError when the server closes the
connection
receive_json kept retrying on EOF until the timeout, then raised a misleading
TimeoutError. It now raises ConnectionError straight away.
This also fixes the flaky subscribe tests: the mock DCS server now keeps the
connection open until the client closes it, and the data-available wait uses
a monotonic clock with a 5 s timeout.
---
src/dsf/connections/base_connection.py | 11 +++++------
tests/test_base_connection.py | 25 ++++++++++++++++++++++++-
tests/test_subscribe_object_model.py | 10 +++++++---
3 files changed, 36 insertions(+), 10 deletions(-)
diff --git a/src/dsf/connections/base_connection.py b/src/dsf/connections/base_connection.py
index 9fda6c1..50a4911 100644
--- a/src/dsf/connections/base_connection.py
+++ b/src/dsf/connections/base_connection.py
@@ -155,16 +155,15 @@ def receive_json(self) -> str:
# Refill the buffer and check again
BUFF_SIZE = 4096 # 4 KiB
data = b""
- part = b""
while True:
try:
part = self.socket.recv(BUFF_SIZE)
- data += part
except socket.timeout:
- pass
- except Exception as e:
- raise e
- # either 0 or end of data
+ break
+ if not part:
+ raise ConnectionError("Connection closed by the server")
+ data += part
+ # end of the currently available data
if len(part) < BUFF_SIZE:
break
diff --git a/tests/test_base_connection.py b/tests/test_base_connection.py
index 160bc56..887590d 100644
--- a/tests/test_base_connection.py
+++ b/tests/test_base_connection.py
@@ -1,4 +1,5 @@
import socket
+import time
import unittest
from unittest.mock import Mock, patch
@@ -30,6 +31,28 @@ def test_has_data_available_returns_true_when_socket_is_readable(self):
):
self.assertTrue(connection.has_data_available())
+ def test_receive_json_returns_first_complete_object_and_buffers_the_rest(self):
+ connection = BaseConnection()
+ client, server = socket.socketpair()
+ with client, server:
+ connection.socket = client
+ server.sendall(b'{"key":1}{"key"')
+
+ self.assertEqual(connection.receive_json(), '{"key":1}')
+ self.assertEqual(connection.input, '{"key"')
+
+ def test_receive_json_raises_when_server_closes_connection(self):
+ connection = BaseConnection(timeout=30)
+ client, server = socket.socketpair()
+ with client:
+ connection.socket = client
+ server.close()
+
+ start = time.monotonic()
+ self.assertRaises(ConnectionError, connection.receive_json)
+ # Must fail immediately instead of waiting for the timeout
+ self.assertLess(time.monotonic() - start, 5)
+
if __name__ == '__main__':
- unittest.main()
\ No newline at end of file
+ unittest.main()
diff --git a/tests/test_subscribe_object_model.py b/tests/test_subscribe_object_model.py
index 606ec8a..efdfde9 100644
--- a/tests/test_subscribe_object_model.py
+++ b/tests/test_subscribe_object_model.py
@@ -19,9 +19,9 @@ class TestSubscribeObjectModel(unittest.TestCase):
"""Test suite for the object model subscription example."""
@staticmethod
- def _wait_for_data_available(subscribe_connection: SubscribeConnection, timeout: float = 1.0) -> bool:
- deadline = time.time() + timeout
- while time.time() < deadline:
+ def _wait_for_data_available(subscribe_connection: SubscribeConnection, timeout: float = 5.0) -> bool:
+ deadline = time.monotonic() + timeout
+ while time.monotonic() < deadline:
if subscribe_connection.has_data_available():
return True
time.sleep(0.01)
@@ -100,6 +100,10 @@ def _run_mock_dcs_server(self):
self.assertEqual(ack, self.acknowledge_response,
f"Expected acknowledge command, received: {ack.decode()}")
+ # Keep the connection open until the client closes it, like DCS does
+ conn.settimeout(5)
+ self.assertEqual(conn.recv(1024), b"", "Expected the client to close the connection")
+
self.dcs_passed.set() # Test completed successfully
except Exception as e:
From 637ff444cc558a87bb1108c511d536044b3f4ac7 Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:11:02 +0100
Subject: [PATCH 10/19] fix: default DriverId to 0.0 and make it hashable
- DriverId() now defaults to board 0, port 0 and hashes by board and port,
matching upstream DSF
- Move the get_kinematics_type docstring to the top of the function
- Let _read_socket_file take the config path so it can be tested
- Add tests for DriverId, kinematics and filament monitor type resolution,
invalid global values, ModelDictionary.from_json and SOCKET_FILE lookup
- Replace the star import in test_object_model with explicit imports
---
src/dsf/__init__.py | 3 +-
src/dsf/object_model/move/driver_id.py | 9 +-
.../move/kinematics/kinematics.py | 11 +-
tests/test_object_model.py | 114 +++++++++++++++++-
tests/test_socket_file.py | 37 ++++++
5 files changed, 159 insertions(+), 15 deletions(-)
create mode 100644 tests/test_socket_file.py
diff --git a/src/dsf/__init__.py b/src/dsf/__init__.py
index 0ed50dc..14bcb43 100644
--- a/src/dsf/__init__.py
+++ b/src/dsf/__init__.py
@@ -4,9 +4,8 @@
import os
-def _read_socket_file() -> str:
+def _read_socket_file(config_path: str = "/opt/dsf/conf/config.json") -> str:
"""Read the socket file path from the DSF config, falling back to the default path"""
- config_path = "/opt/dsf/conf/config.json"
if os.path.exists(config_path):
try:
with open(config_path, 'r') as f:
diff --git a/src/dsf/object_model/move/driver_id.py b/src/dsf/object_model/move/driver_id.py
index a2471b6..db3983c 100644
--- a/src/dsf/object_model/move/driver_id.py
+++ b/src/dsf/object_model/move/driver_id.py
@@ -24,10 +24,8 @@ class DriverId(ModelObject):
def __init__(self, as_str: Optional[str] = None, as_int: Optional[int] = None, board: Optional[int] = None, port: Optional[int] = None):
super().__init__()
- if board is not None:
- self.board = board
- if port is not None:
- self.port = port
+ self.board = board if board is not None else 0
+ self.port = port if port is not None else 0
if as_int is not None:
if as_int < 0:
@@ -63,6 +61,9 @@ def __eq__(self, o: object) -> bool:
def __ne__(self, o: object) -> bool:
return not self == o
+ def __hash__(self) -> int:
+ return hash((self.board, self.port))
+
def update_from_json(self, data: JSONObj | str) -> Self:
if not isinstance(data, str):
raise TypeError(f"DriverId must be updated from a string. Got {type(data).__name__}: {data}")
diff --git a/src/dsf/object_model/move/kinematics/kinematics.py b/src/dsf/object_model/move/kinematics/kinematics.py
index bb6639b..3fba4e7 100644
--- a/src/dsf/object_model/move/kinematics/kinematics.py
+++ b/src/dsf/object_model/move/kinematics/kinematics.py
@@ -18,16 +18,17 @@ def __init__(self, name: KinematicsName = KinematicsName.unknown):
@staticmethod
def get_kinematics_type(name: KinematicsName | str) -> "Kinematics":
- from .core_kinematics import CoreKinematics
- from .delta_kinematics import DeltaKinematics
- from .hangprinter_kinematics import HangprinterKinematics
- from .polar_kinematics import PolarKinematics
- from .scara_kinematics import ScaraKinematics
"""
Figure out the required type for the given kinematics name
:param name: Kinematics name
:returns: Required type
"""
+ from .core_kinematics import CoreKinematics
+ from .delta_kinematics import DeltaKinematics
+ from .hangprinter_kinematics import HangprinterKinematics
+ from .polar_kinematics import PolarKinematics
+ from .scara_kinematics import ScaraKinematics
+
name = KinematicsName(name.lower().replace(' ', ''))
if name in [
diff --git a/tests/test_object_model.py b/tests/test_object_model.py
index 7564e28..51012e2 100644
--- a/tests/test_object_model.py
+++ b/tests/test_object_model.py
@@ -3,10 +3,13 @@
from typing import Optional, cast
-from src.dsf.object_model import *
+from src.dsf.object_model import (
+ Accelerometer, Boards, BoardState, DriverId, ExpansionBoard, Heater, InputChannel, MainBoard, ObjectModel, Plugin,
+ ProbeLoadCell, ProbeType)
from src.dsf.object_model.utils import is_model_object, JSONElement, JSONObj, model_prop, nullable_model_prop
from src.dsf.object_model.object_model import ModelCollection, ModelDictionary, ModelObject
+
class SubModel(ModelObject):
value = model_prop("value", int, 0)
@@ -27,8 +30,10 @@ class Dummy(ModelObject):
p_model_collection = model_prop("p_model_collection", ModelCollection[SubModel], ModelCollection(SubModel))
p_model_ncollection = model_prop("p_model_ncollection", ModelCollection[Optional[SubModel]], ModelCollection(Optional[SubModel]))
- np_model_collection = nullable_model_prop("np_model_collection", ModelCollection[SubModel], lambda: ModelCollection(SubModel))
- np_model_ncollection = nullable_model_prop("np_model_ncollection", ModelCollection[Optional[SubModel]], lambda: ModelCollection(Optional[SubModel]))
+ np_model_collection = nullable_model_prop(
+ "np_model_collection", ModelCollection[SubModel], lambda: ModelCollection(SubModel))
+ np_model_ncollection = nullable_model_prop(
+ "np_model_ncollection", ModelCollection[Optional[SubModel]], lambda: ModelCollection(Optional[SubModel]))
def setUp(self):
pass
@@ -283,6 +288,17 @@ def setUp(self):
def tearDown(self):
pass
+ def test_from_json(self):
+ model = ModelDictionary.from_json({"key1": 1, "key2": {"nested": "dict"}})
+ self.assertIsInstance(model, ModelDictionary)
+ self.assertEqual(dict(model), {"key1": 1, "key2": {"nested": "dict"}})
+
+ # Keys matching Python builtins are kept as-is
+ self.assertEqual(dict(ModelDictionary.from_json({"type": 1})), {"type": 1})
+
+ # null creates an empty dictionary
+ self.assertEqual(len(ModelDictionary.from_json(None)), 0)
+
def test_generic_dict(self):
model = ModelDictionary(False)
@@ -336,6 +352,39 @@ def test_model_object_dict(self):
self.assertRaises(TypeError, lambda: model.update_from_json({"item1": 1})) # can't update a model object with a non-dict value
+class TestDriverId(unittest.TestCase):
+ def test_defaults(self):
+ driver = DriverId()
+ self.assertEqual((driver.board, driver.port), (0, 0))
+ self.assertEqual(driver.as_int(), 0)
+ self.assertEqual(str(driver), "0.0")
+
+ def test_constructors(self):
+ self.assertEqual(DriverId(as_str="3"), DriverId(board=0, port=3))
+ self.assertEqual(DriverId(as_str="1.2"), DriverId(board=1, port=2))
+ self.assertEqual(DriverId(as_int=(1 << 16) | 2), DriverId(board=1, port=2))
+ self.assertEqual(DriverId(board=1, port=2).as_int(), (1 << 16) | 2)
+
+ def test_update_from_json(self):
+ driver = DriverId(board=5, port=5)
+ self.assertIs(driver.update_from_json("1.2"), driver)
+ self.assertEqual((driver.board, driver.port), (1, 2))
+
+ # A port-only string refers to the main board
+ driver.update_from_json("3")
+ self.assertEqual((driver.board, driver.port), (0, 3))
+ self.assertEqual(driver.as_int(), 3)
+ self.assertEqual(str(driver), "0.3")
+
+ self.assertRaises(TypeError, lambda: driver.update_from_json({"board": 1, "port": 2}))
+
+ def test_equality_and_hash(self):
+ self.assertEqual(DriverId(board=1, port=2), DriverId(as_str="1.2"))
+ self.assertNotEqual(DriverId(board=1, port=2), DriverId(board=2, port=1))
+ self.assertNotEqual(DriverId(board=1, port=2), "1.2")
+ self.assertEqual(len({DriverId(board=1, port=2), DriverId(as_str="1.2"), DriverId(board=0, port=2)}), 2)
+
+
class Model(unittest.TestCase):
def setUp(self):
@@ -483,6 +532,10 @@ def test_global(self):
model.update_from_json('{"global":null}')
self.assertEqual(len(model.globals), 0)
+ # Anything other than an object or null is rejected
+ self.assertRaises(TypeError, lambda: model.update_from_json('{"global":[1, 2]}'))
+ self.assertRaises(TypeError, lambda: model.update_from_json('{"global":5}'))
+
def test_reserved_keys(self):
# JSON keys shadowing Python builtins get a trailing underscore while being unpacked
# (see preserve_builtin) and must still map to the correct properties
@@ -622,7 +675,7 @@ def test_messages(self):
self.assertEqual(len(model.messages), 0)
def test_move_kinematics(self):
- from src.dsf.object_model.move.kinematics import CoreKinematics, DeltaKinematics, KinematicsName
+ from src.dsf.object_model.move.kinematics import CoreKinematics, DeltaKinematics, Kinematics, KinematicsName
model = ObjectModel()
json_patch = '{"move": {"kinematics": {"name": "delta","deltaRadius": 123}}}'
@@ -645,6 +698,38 @@ def test_move_kinematics(self):
self.assertEqual(model.move.kinematics.name, KinematicsName.linearDelta)
self.assertEqual(model.move.kinematics.delta_radius, 105.6)
+ # Kinematics without a dedicated class use the base type (eg: M669 K0 on an unconfigured machine)
+ model.update_from_json('{"move":{"kinematics":{"name":"unknown"}}}')
+ self.assertIs(type(model.move.kinematics), Kinematics)
+ self.assertEqual(model.move.kinematics.name, KinematicsName.unknown)
+
+ def test_get_kinematics_type(self):
+ from src.dsf.object_model.move.kinematics import (
+ CoreKinematics, DeltaKinematics, HangprinterKinematics, Kinematics, KinematicsName, PolarKinematics,
+ ScaraKinematics)
+
+ expected_types = {
+ KinematicsName.cartesian: CoreKinematics,
+ KinematicsName.coreXY: CoreKinematics,
+ KinematicsName.markForged: CoreKinematics,
+ KinematicsName.linearDelta: DeltaKinematics,
+ KinematicsName.rotaryDelta: Kinematics,
+ KinematicsName.hangprinter: HangprinterKinematics,
+ KinematicsName.fiveBarScara: ScaraKinematics,
+ KinematicsName.scara: ScaraKinematics,
+ KinematicsName.polar: PolarKinematics,
+ KinematicsName.unknown: Kinematics,
+ }
+ for name, expected_type in expected_types.items():
+ kinematics = Kinematics.get_kinematics_type(name)
+ self.assertIs(type(kinematics), expected_type, name)
+ self.assertEqual(kinematics.name, name)
+
+ # Names reported by RRF are normalized
+ self.assertIs(type(Kinematics.get_kinematics_type("Core XY")), CoreKinematics)
+ self.assertEqual(Kinematics.get_kinematics_type("Rotary Delta").name, KinematicsName.rotaryDelta)
+ self.assertRaises(ValueError, lambda: Kinematics.get_kinematics_type("not a kinematics"))
+
def test_plugins(self):
model = ObjectModel()
self.assertEqual(len(model.plugins), 0)
@@ -690,6 +775,27 @@ def test_sensors_filament_monitor(self):
assert filament_monitor is not None
self.assertEqual(filament_monitor.type, FilamentMonitorType.Pulsed)
+ def test_get_filament_monitor(self):
+ from src.dsf.object_model.sensors.filament_monitors import (
+ FilamentMonitor, FilamentMonitorType, LaserFilamentMonitor, PulsedFilamentMonitor,
+ RotatingMagnetFilamentMonitor)
+
+ expected_types = {
+ FilamentMonitorType.Laser: LaserFilamentMonitor,
+ FilamentMonitorType.Pulsed: PulsedFilamentMonitor,
+ FilamentMonitorType.RotatingMagnet: RotatingMagnetFilamentMonitor,
+ FilamentMonitorType.Simple: FilamentMonitor,
+ FilamentMonitorType.Unknown: FilamentMonitor,
+ }
+ for monitor_type, expected_type in expected_types.items():
+ # Both the enum and its JSON string value are accepted
+ for type_ in (monitor_type, monitor_type.value):
+ monitor = FilamentMonitor.get_filament_monitor(type_)
+ self.assertIs(type(monitor), expected_type, type_)
+ self.assertEqual(monitor.type, monitor_type)
+
+ self.assertRaises(ValueError, lambda: FilamentMonitor.get_filament_monitor("not a monitor"))
+
def test_sensors_filament_monitor_rc2_fields(self):
from src.dsf.object_model.sensors.filament_monitors import RotatingMagnetFilamentMonitor
diff --git a/tests/test_socket_file.py b/tests/test_socket_file.py
new file mode 100644
index 0000000..e613f53
--- /dev/null
+++ b/tests/test_socket_file.py
@@ -0,0 +1,37 @@
+import json
+import os
+import tempfile
+import unittest
+
+from src.dsf import _read_socket_file # pyright: ignore[reportPrivateUsage]
+
+
+class TestSocketFile(unittest.TestCase):
+ def setUp(self):
+ self.tmp_dir = tempfile.TemporaryDirectory()
+ self.config_path = os.path.join(self.tmp_dir.name, "config.json")
+
+ def tearDown(self):
+ self.tmp_dir.cleanup()
+
+ def test_default_without_config(self):
+ self.assertEqual(_read_socket_file(self.config_path), "/run/dsf/dcs.sock")
+
+ def test_reads_socket_path_from_config(self):
+ with open(self.config_path, "w") as f:
+ json.dump({"SocketDirectory": "/var/run/custom", "SocketFile": "custom.sock"}, f)
+ self.assertEqual(_read_socket_file(self.config_path), "/var/run/custom/custom.sock")
+
+ def test_missing_config_keys_use_defaults(self):
+ with open(self.config_path, "w") as f:
+ json.dump({"SocketFile": "custom.sock"}, f)
+ self.assertEqual(_read_socket_file(self.config_path), "/run/dsf/custom.sock")
+
+ def test_invalid_config_uses_default(self):
+ with open(self.config_path, "w") as f:
+ f.write("{not json")
+ self.assertEqual(_read_socket_file(self.config_path), "/run/dsf/dcs.sock")
+
+
+if __name__ == '__main__':
+ unittest.main()
From 4bad8280fc08fa1936474590ba307118848704b6 Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:11:02 +0100
Subject: [PATCH 11/19] style: wrap long lines
Wrap long __all__ lists and function signatures to satisfy flake8's
120 character limit.
---
src/dsf/commands/__init__.py | 17 +++++-
src/dsf/connections/__init__.py | 13 +++-
src/dsf/object_model/__init__.py | 60 ++++++++++++++++++-
src/dsf/object_model/model_collection.py | 7 ++-
src/dsf/object_model/model_dictionary.py | 10 +++-
src/dsf/object_model/move/__init__.py | 23 ++++++-
src/dsf/object_model/sensors/__init__.py | 13 +++-
.../sensors/filament_monitors/__init__.py | 16 ++++-
src/dsf/object_model/state/__init__.py | 12 +++-
src/dsf/object_model/utils.py | 9 ++-
10 files changed, 168 insertions(+), 12 deletions(-)
diff --git a/src/dsf/commands/__init__.py b/src/dsf/commands/__init__.py
index 01d16e2..f5166c1 100644
--- a/src/dsf/commands/__init__.py
+++ b/src/dsf/commands/__init__.py
@@ -2,4 +2,19 @@
http_endpoints, model_subscription, object_model, packages, plugins, responses, user_sessions
-__all__ = ['base_command', 'code', 'code_interception', 'code_channel', 'code_parameter', 'files', 'generic', 'http_endpoints', 'model_subscription', 'object_model', 'packages', 'plugins', 'responses', 'user_sessions']
+__all__ = [
+ 'base_command',
+ 'code',
+ 'code_interception',
+ 'code_channel',
+ 'code_parameter',
+ 'files',
+ 'generic',
+ 'http_endpoints',
+ 'model_subscription',
+ 'object_model',
+ 'packages',
+ 'plugins',
+ 'responses',
+ 'user_sessions',
+]
diff --git a/src/dsf/connections/__init__.py b/src/dsf/connections/__init__.py
index 027cc88..d3f0758 100644
--- a/src/dsf/connections/__init__.py
+++ b/src/dsf/connections/__init__.py
@@ -52,4 +52,15 @@ class SubscriptionMode(str, Enum):
from .subscribe_connection import SubscribeConnection
-__all__ = ['ConnectionMode', 'InterceptionMode', 'SubscriptionMode', 'BaseCommandConnection', 'BaseConnection', 'CommandConnection', 'InternalServerException', 'TaskCanceledException', 'InterceptConnection', 'SubscribeConnection']
+__all__ = [
+ 'ConnectionMode',
+ 'InterceptionMode',
+ 'SubscriptionMode',
+ 'BaseCommandConnection',
+ 'BaseConnection',
+ 'CommandConnection',
+ 'InternalServerException',
+ 'TaskCanceledException',
+ 'InterceptConnection',
+ 'SubscribeConnection',
+]
diff --git a/src/dsf/object_model/__init__.py b/src/dsf/object_model/__init__.py
index 1111aa4..db52d94 100644
--- a/src/dsf/object_model/__init__.py
+++ b/src/dsf/object_model/__init__.py
@@ -20,4 +20,62 @@
from .volumes import Volume
-__all__ = ['Board', 'Boards', 'BoardState', 'ExpansionBoard', 'MainBoard', 'Directories', 'Fan', 'Heat', 'Heater', 'HeaterState', 'InputChannel', 'Job', 'LedStrip', 'LedStripType', 'Limits', 'Message', 'MessageType', 'DriverId', 'Move', 'Network', 'NetworkInterface', 'NetworkInterfaceType', 'NetworkProtocol', 'NetworkState', 'ObjectModel', 'Plugin', 'PluginManifest', 'SbcPermissions', 'CPU', 'Memory', 'SBC', 'Upgrade', 'AccessLevel', 'HttpEndpoint', 'HttpEndpointType', 'SessionType', 'UserSession', 'Accelerometer', 'AnalogSensor', 'AnalogSensorType', 'Endstop', 'EndstopType', 'GpInputPort', 'Probe', 'ProbeLoadCell', 'ProbeType', 'Sensors', 'Spindle', 'SpindleState', 'LogLevel', 'MachineStatus', 'MessageBox', 'MessageBoxMode', 'State', 'Tool', 'ToolState', 'Volume']
+__all__ = [
+ 'Board',
+ 'Boards',
+ 'BoardState',
+ 'ExpansionBoard',
+ 'MainBoard',
+ 'Directories',
+ 'Fan',
+ 'Heat',
+ 'Heater',
+ 'HeaterState',
+ 'InputChannel',
+ 'Job',
+ 'LedStrip',
+ 'LedStripType',
+ 'Limits',
+ 'Message',
+ 'MessageType',
+ 'DriverId',
+ 'Move',
+ 'Network',
+ 'NetworkInterface',
+ 'NetworkInterfaceType',
+ 'NetworkProtocol',
+ 'NetworkState',
+ 'ObjectModel',
+ 'Plugin',
+ 'PluginManifest',
+ 'SbcPermissions',
+ 'CPU',
+ 'Memory',
+ 'SBC',
+ 'Upgrade',
+ 'AccessLevel',
+ 'HttpEndpoint',
+ 'HttpEndpointType',
+ 'SessionType',
+ 'UserSession',
+ 'Accelerometer',
+ 'AnalogSensor',
+ 'AnalogSensorType',
+ 'Endstop',
+ 'EndstopType',
+ 'GpInputPort',
+ 'Probe',
+ 'ProbeLoadCell',
+ 'ProbeType',
+ 'Sensors',
+ 'Spindle',
+ 'SpindleState',
+ 'LogLevel',
+ 'MachineStatus',
+ 'MessageBox',
+ 'MessageBoxMode',
+ 'State',
+ 'Tool',
+ 'ToolState',
+ 'Volume',
+]
diff --git a/src/dsf/object_model/model_collection.py b/src/dsf/object_model/model_collection.py
index 2fdc41e..31544ae 100644
--- a/src/dsf/object_model/model_collection.py
+++ b/src/dsf/object_model/model_collection.py
@@ -13,7 +13,12 @@ class ModelCollection(ModelType[list[JSONElement]], Generic[T], list[T]):
Useful for updating model object items from JSON data (patches)
"""
- def __init__(self, item_constructor: type[T] | object, value: Optional[Sequence[T | JSONElement]] = None, allow_none: bool = False) -> None:
+ def __init__(
+ self,
+ item_constructor: type[T] | object,
+ value: Optional[Sequence[T | JSONElement]] = None,
+ allow_none: bool = False,
+ ) -> None:
"""
:param item_constructor: Item constructor type that items must derive from
:param value: Value used to initialize the list from
diff --git a/src/dsf/object_model/model_dictionary.py b/src/dsf/object_model/model_dictionary.py
index bf88195..fbbf6c3 100644
--- a/src/dsf/object_model/model_dictionary.py
+++ b/src/dsf/object_model/model_dictionary.py
@@ -13,7 +13,12 @@ class ModelDictionary(ModelType[JSONObj], dict[str, Any]):
Useful for updating model object items from JSON data (patches)
"""
- def __init__(self, null_deletes_keys: bool, item_constructor: Optional[type["ModelObject"]] = None, value: Optional[dict[str, Any]] = None):
+ def __init__(
+ self,
+ null_deletes_keys: bool,
+ item_constructor: Optional[type["ModelObject"]] = None,
+ value: Optional[dict[str, Any]] = None,
+ ):
"""
:param null_deletes_keys: Whether setting null to items effectively deletes them
:param item_constructor: Item constructor type to use for type-checking
@@ -39,7 +44,8 @@ def __setitem__(self, key: str, value: Any) -> None:
current_item = self.get(key)
if current_item is None and self._item_constructor:
if not isinstance(value, dict):
- raise TypeError(f"Value for key '{key}' must be of type dict to update the model object. Got {type(value)}: {value}")
+ raise TypeError(f"Value for key '{key}' must be of type dict to update the model object."
+ f" Got {type(value)}: {value}")
new_item = self._item_constructor()
return super().__setitem__(key, new_item.update_from_json(cast(JSONObj, value)))
elif is_model_object(current_item):
diff --git a/src/dsf/object_model/move/__init__.py b/src/dsf/object_model/move/__init__.py
index cfbb647..ca2d1e7 100644
--- a/src/dsf/object_model/move/__init__.py
+++ b/src/dsf/object_model/move/__init__.py
@@ -17,4 +17,25 @@
from .skew import Skew
-__all__ = ['Axis', 'AxisLetter', 'CurrentMove', 'DriverId', 'Extruder', 'ExtruderNonlinear', 'InputShaping', 'InputShapingType', 'MicroStepping', 'MotorsIdleControl', 'Move', 'MoveCalibration', 'MoveCompensation', 'MoveCompensationType', 'MoveDeviations', 'MoveQueueItem', 'MoveRotation', 'MoveSegmentation', 'ProbeGrid', 'Skew']
+__all__ = [
+ 'Axis',
+ 'AxisLetter',
+ 'CurrentMove',
+ 'DriverId',
+ 'Extruder',
+ 'ExtruderNonlinear',
+ 'InputShaping',
+ 'InputShapingType',
+ 'MicroStepping',
+ 'MotorsIdleControl',
+ 'Move',
+ 'MoveCalibration',
+ 'MoveCompensation',
+ 'MoveCompensationType',
+ 'MoveDeviations',
+ 'MoveQueueItem',
+ 'MoveRotation',
+ 'MoveSegmentation',
+ 'ProbeGrid',
+ 'Skew',
+]
diff --git a/src/dsf/object_model/sensors/__init__.py b/src/dsf/object_model/sensors/__init__.py
index 6fd1677..d551a39 100644
--- a/src/dsf/object_model/sensors/__init__.py
+++ b/src/dsf/object_model/sensors/__init__.py
@@ -10,4 +10,15 @@
from .sensors import Sensors
-__all__ = ['Accelerometer', 'AnalogSensor', 'AnalogSensorType', 'Endstop', 'EndstopType', 'GpInputPort', 'Probe', 'ProbeLoadCell', 'ProbeType', 'Sensors']
+__all__ = [
+ 'Accelerometer',
+ 'AnalogSensor',
+ 'AnalogSensorType',
+ 'Endstop',
+ 'EndstopType',
+ 'GpInputPort',
+ 'Probe',
+ 'ProbeLoadCell',
+ 'ProbeType',
+ 'Sensors',
+]
diff --git a/src/dsf/object_model/sensors/filament_monitors/__init__.py b/src/dsf/object_model/sensors/filament_monitors/__init__.py
index 921e179..17d8979 100644
--- a/src/dsf/object_model/sensors/filament_monitors/__init__.py
+++ b/src/dsf/object_model/sensors/filament_monitors/__init__.py
@@ -16,4 +16,18 @@
'RotatingMagnetFilamentMonitorConfigured']
-__all__ = ['Duet3DFilamentMonitor', 'FilamentMonitor', 'FilamentMonitorStatus', 'FilamentMonitorType', 'LaserFilamentMonitor', 'LaserFilamentMonitorCalibrated', 'LaserFilamentMonitorConfigured', 'PulsedFilamentMonitor', 'PulsedFilamentMonitorCalibrated', 'PulsedFilamentMonitorConfigured', 'RotatingMagnetFilamentMonitor', 'RotatingMagnetFilamentMonitorCalibrated', 'RotatingMagnetFilamentMonitorConfigured']
+__all__ = [
+ 'Duet3DFilamentMonitor',
+ 'FilamentMonitor',
+ 'FilamentMonitorStatus',
+ 'FilamentMonitorType',
+ 'LaserFilamentMonitor',
+ 'LaserFilamentMonitorCalibrated',
+ 'LaserFilamentMonitorConfigured',
+ 'PulsedFilamentMonitor',
+ 'PulsedFilamentMonitorCalibrated',
+ 'PulsedFilamentMonitorConfigured',
+ 'RotatingMagnetFilamentMonitor',
+ 'RotatingMagnetFilamentMonitorCalibrated',
+ 'RotatingMagnetFilamentMonitorConfigured',
+]
diff --git a/src/dsf/object_model/state/__init__.py b/src/dsf/object_model/state/__init__.py
index 97fec59..f25d396 100644
--- a/src/dsf/object_model/state/__init__.py
+++ b/src/dsf/object_model/state/__init__.py
@@ -8,4 +8,14 @@
from .state import State
-__all__ = ['BeepRequest', 'GpOutputPort', 'LogLevel', 'MachineStatus', 'MessageBox', 'MessageBoxMode', 'RestorePoint', 'StartupError', 'State']
+__all__ = [
+ 'BeepRequest',
+ 'GpOutputPort',
+ 'LogLevel',
+ 'MachineStatus',
+ 'MessageBox',
+ 'MessageBoxMode',
+ 'RestorePoint',
+ 'StartupError',
+ 'State',
+]
diff --git a/src/dsf/object_model/utils.py b/src/dsf/object_model/utils.py
index 481e5aa..a7c46cf 100644
--- a/src/dsf/object_model/utils.py
+++ b/src/dsf/object_model/utils.py
@@ -33,7 +33,9 @@ def is_model_object(o: object) -> TypeGuard[ModelType[Any]]:
return isinstance(o, ModelObject) or isinstance(o, ModelCollection) or isinstance(o, ModelDictionary)
-def _set_model_prop(instance: object, name: str, runtime_type: type[object], current_value: T, value: Union[T, JSONElement]) -> None:
+def _set_model_prop(
+ instance: object, name: str, runtime_type: type[object], current_value: T, value: Union[T, JSONElement]
+) -> None:
if value is None and isinstance(current_value, ModelDictionary): # DSF sends null to clear a dictionary
current_value.update_from_json(None)
elif isinstance(value, dict): # Update from JSON
@@ -109,7 +111,10 @@ def get_or_create_value() -> T:
return cast(TypedReadableProperty[T], property(getter, setter))
-def nullable_model_prop(name: str, model_type: type[T], constructor: Optional[Callable[[], Optional[T]]] = None) -> TypedReadableProperty[Optional[T]]:
+
+def nullable_model_prop(
+ name: str, model_type: type[T], constructor: Optional[Callable[[], Optional[T]]] = None
+) -> TypedReadableProperty[Optional[T]]:
"""
Wrap a nullable model object property so that type checks can be performed during update
:param name: Property of the derived class
From 425a2ffa431b407684bea0214c20772f1ad36204 Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:20:01 +0100
Subject: [PATCH 12/19] style: format code with black
Mechanical reformat with black 26.5.1 (line length 120). No functional changes.
- Configure black's line length in pyproject.toml
- Pin black in tox.ini
---
conftest.py | 2 +-
examples/subscribe_object_model.py | 4 +-
pyproject.toml | 3 +
src/dsf/__init__.py | 5 +-
src/dsf/commands/__init__.py | 47 ++-
src/dsf/commands/base_command.py | 1 +
src/dsf/commands/code.py | 2 +-
src/dsf/commands/code_channel.py | 2 +-
src/dsf/commands/code_parameter.py | 2 +-
src/dsf/commands/generic.py | 5 +-
src/dsf/commands/plugins.py | 4 +-
src/dsf/commands/responses.py | 5 +
src/dsf/connections/__init__.py | 21 +-
.../connections/base_command_connection.py | 13 +-
src/dsf/connections/base_connection.py | 7 +-
src/dsf/connections/exceptions.py | 1 +
src/dsf/connections/init_messages/__init__.py | 3 +-
.../init_messages/client_init_messages.py | 22 +-
.../init_messages/server_init_message.py | 2 +-
src/dsf/connections/intercept_connection.py | 4 +-
src/dsf/connections/subscribe_connection.py | 16 +-
src/dsf/http.py | 17 +-
src/dsf/object_model/__init__.py | 128 +++----
src/dsf/object_model/boards/__init__.py | 16 +-
src/dsf/object_model/boards/boards.py | 48 +--
.../boards/direct_display/__init__.py | 9 +-
.../boards/direct_display/direct_display.py | 6 +-
.../direct_display/direct_display_screen.py | 22 +-
src/dsf/object_model/boards/driver.py | 4 +-
.../object_model/boards/driver_closed_loop.py | 2 +-
src/dsf/object_model/boards/driver_config.py | 4 +-
src/dsf/object_model/boards/driver_mode.py | 12 +-
.../object_model/boards/min_max_current.py | 4 +-
src/dsf/object_model/directories/__init__.py | 2 +-
src/dsf/object_model/fans/__init__.py | 2 +-
src/dsf/object_model/heat/__init__.py | 12 +-
src/dsf/object_model/heat/heat.py | 4 +-
src/dsf/object_model/inputs/__init__.py | 2 +-
src/dsf/object_model/job/__init__.py | 3 +-
src/dsf/object_model/job/job.py | 34 +-
src/dsf/object_model/led_strips/__init__.py | 3 +-
src/dsf/object_model/led_strips/led_strip.py | 12 +-
.../led_strips/led_strip_color_order.py | 13 +-
src/dsf/object_model/limits/__init__.py | 2 +-
src/dsf/object_model/messages/__init__.py | 2 +-
src/dsf/object_model/model_collection.py | 29 +-
src/dsf/object_model/model_dictionary.py | 6 +-
src/dsf/object_model/model_object.py | 14 +-
src/dsf/object_model/model_type.py | 1 +
src/dsf/object_model/move/__init__.py | 41 ++-
src/dsf/object_model/move/axis.py | 76 ++---
src/dsf/object_model/move/driver_id.py | 11 +-
src/dsf/object_model/move/extruder.py | 6 +-
.../move/extruder_pressure_advance.py | 2 +-
src/dsf/object_model/move/input_shaping.py | 4 +-
.../object_model/move/kinematics/__init__.py | 14 +-
.../move/kinematics/core_kinematics.py | 12 +-
.../move/kinematics/hangprinter_kinematics.py | 10 +-
.../move/kinematics/kinematics.py | 10 +-
.../move/kinematics/polar_kinematics.py | 3 +-
.../move/kinematics/scara_kinematics.py | 1 -
src/dsf/object_model/move/motion_system.py | 1 +
src/dsf/object_model/move/move.py | 48 +--
.../object_model/move/move_compensation.py | 16 +-
src/dsf/object_model/move/probe_grid.py | 2 +-
src/dsf/object_model/network/__init__.py | 2 +-
.../object_model/network/network_interface.py | 36 +-
src/dsf/object_model/object_model.py | 45 ++-
src/dsf/object_model/plugins/__init__.py | 3 +-
.../object_model/plugins/plugin_manifest.py | 54 +--
src/dsf/object_model/sbc/__init__.py | 3 +-
src/dsf/object_model/sbc/cpu.py | 1 -
src/dsf/object_model/sbc/dsf/__init__.py | 3 +-
.../sbc/dsf/communication_method.py | 2 +-
src/dsf/object_model/sbc/dsf/dsf.py | 16 +-
src/dsf/object_model/sbc/dsf/http_endpoint.py | 1 -
.../sbc/dsf/user_sessions/__init__.py | 3 +-
src/dsf/object_model/sbc/sbc.py | 22 +-
src/dsf/object_model/sbc/upgrade.py | 4 +-
src/dsf/object_model/sensors/__init__.py | 21 +-
src/dsf/object_model/sensors/analog_sensor.py | 26 +-
.../sensors/analog_sensor_type.py | 4 +-
.../sensors/filament_monitors/__init__.py | 61 ++--
.../filament_monitors/filament_monitor.py | 2 +-
.../laser_filament_monitor.py | 4 +-
.../pulsed_filament_monitor.py | 6 +-
.../rotating_magnet_filament_monitor.py | 6 +-
src/dsf/object_model/sensors/probe.py | 46 +--
.../object_model/sensors/probe_touch_mode.py | 2 +-
src/dsf/object_model/sensors/sensors.py | 8 +-
src/dsf/object_model/spindles/__init__.py | 2 +-
src/dsf/object_model/state/__init__.py | 19 +-
src/dsf/object_model/state/message_box.py | 22 +-
src/dsf/object_model/state/restore_point.py | 16 +-
src/dsf/object_model/state/state.py | 50 +--
src/dsf/object_model/tools/__init__.py | 2 +-
src/dsf/object_model/tools/tool_retraction.py | 2 +-
src/dsf/object_model/tools/tools.py | 42 +--
src/dsf/object_model/utils.py | 40 ++-
src/dsf/object_model/volumes/__init__.py | 2 +-
src/dsf/object_model/volumes/volumes.py | 30 +-
src/dsf/utils.py | 26 +-
tests/test_base_connection.py | 6 +-
tests/test_code.py | 2 +-
tests/test_commands.py | 36 +-
tests/test_custom_http_endpoint.py | 9 +-
tests/test_custom_m_codes.py | 25 +-
tests/test_object_model.py | 322 ++++++++++--------
tests/test_send_simple_code.py | 12 +-
tests/test_socket_file.py | 2 +-
tests/test_subscribe_object_model.py | 9 +-
tests/utils.py | 4 +-
tox.ini | 2 +-
113 files changed, 1036 insertions(+), 868 deletions(-)
diff --git a/conftest.py b/conftest.py
index 8ab6c3e..cd110c9 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1 +1 @@
-# Used to help pytest discover tests
\ No newline at end of file
+# Used to help pytest discover tests
diff --git a/examples/subscribe_object_model.py b/examples/subscribe_object_model.py
index 740becd..db59dc0 100755
--- a/examples/subscribe_object_model.py
+++ b/examples/subscribe_object_model.py
@@ -38,7 +38,9 @@ def subscribe() -> None:
print("Machine status is", object_model.state.status.value)
# Register callbacks for the keys we are interested in, ^ matches any list index
- unsubscribe_status: Callable[[], None] = subscribe_connection.subscribe_to_keys(["state.status"], on_status_changed)
+ unsubscribe_status: Callable[[], None] = subscribe_connection.subscribe_to_keys(
+ ["state.status"], on_status_changed
+ )
subscribe_connection.subscribe_to_keys(["heat.heaters.^.current"], on_heater_temperature_changed)
for _ in range(20):
diff --git a/pyproject.toml b/pyproject.toml
index 9a9f2ab..6555f68 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,3 +11,6 @@ include = ["src", "tests", "examples"]
extraPaths = ["src"]
pythonVersion = "3.11"
typeCheckingMode = "strict"
+
+[tool.black]
+line-length = 120
diff --git a/src/dsf/__init__.py b/src/dsf/__init__.py
index 14bcb43..621823a 100644
--- a/src/dsf/__init__.py
+++ b/src/dsf/__init__.py
@@ -8,7 +8,7 @@ def _read_socket_file(config_path: str = "/opt/dsf/conf/config.json") -> str:
"""Read the socket file path from the DSF config, falling back to the default path"""
if os.path.exists(config_path):
try:
- with open(config_path, 'r') as f:
+ with open(config_path, "r") as f:
config = json.load(f)
socket_dir: str = config.get("SocketDirectory", "/run/dsf")
socket_file: str = config.get("SocketFile", "dcs.sock")
@@ -29,5 +29,4 @@ def _read_socket_file(config_path: str = "/opt/dsf/conf/config.json") -> str:
from . import commands, connections, http, object_model
-
-__all__ = ['SOCKET_FILE', 'DEFAULT_BACKLOG', 'PROTOCOL_VERSION', 'commands', 'connections', 'http', 'object_model']
+__all__ = ["SOCKET_FILE", "DEFAULT_BACKLOG", "PROTOCOL_VERSION", "commands", "connections", "http", "object_model"]
diff --git a/src/dsf/commands/__init__.py b/src/dsf/commands/__init__.py
index f5166c1..684121f 100644
--- a/src/dsf/commands/__init__.py
+++ b/src/dsf/commands/__init__.py
@@ -1,20 +1,33 @@
-from . import base_command, code, code_interception, code_channel, code_parameter, files, generic,\
- http_endpoints, model_subscription, object_model, packages, plugins, responses, user_sessions
-
+from . import (
+ base_command,
+ code,
+ code_interception,
+ code_channel,
+ code_parameter,
+ files,
+ generic,
+ http_endpoints,
+ model_subscription,
+ object_model,
+ packages,
+ plugins,
+ responses,
+ user_sessions,
+)
__all__ = [
- 'base_command',
- 'code',
- 'code_interception',
- 'code_channel',
- 'code_parameter',
- 'files',
- 'generic',
- 'http_endpoints',
- 'model_subscription',
- 'object_model',
- 'packages',
- 'plugins',
- 'responses',
- 'user_sessions',
+ "base_command",
+ "code",
+ "code_interception",
+ "code_channel",
+ "code_parameter",
+ "files",
+ "generic",
+ "http_endpoints",
+ "model_subscription",
+ "object_model",
+ "packages",
+ "plugins",
+ "responses",
+ "user_sessions",
]
diff --git a/src/dsf/commands/base_command.py b/src/dsf/commands/base_command.py
index b6dd115..18e5b49 100644
--- a/src/dsf/commands/base_command.py
+++ b/src/dsf/commands/base_command.py
@@ -1,5 +1,6 @@
from typing import Any
+
class BaseCommand:
"""Base class of a command."""
diff --git a/src/dsf/commands/code.py b/src/dsf/commands/code.py
index d2b29d2..bd4a87e 100644
--- a/src/dsf/commands/code.py
+++ b/src/dsf/commands/code.py
@@ -177,4 +177,4 @@ def keyword_to_str(self) -> Optional[str]:
}.get(self.keyword)
def is_flag_set(self, flag: CodeFlags) -> bool:
- return self.flags & flag != 0
\ No newline at end of file
+ return self.flags & flag != 0
diff --git a/src/dsf/commands/code_channel.py b/src/dsf/commands/code_channel.py
index 83daef4..ddf2057 100644
--- a/src/dsf/commands/code_channel.py
+++ b/src/dsf/commands/code_channel.py
@@ -56,7 +56,7 @@ class CodeChannel(str, Enum):
@staticmethod
def list():
return list(map(lambda cc: cc, CodeChannel))
-
+
def get_input_index(self) -> int:
"""Get the index of this code channel for use in client init messages"""
return self.list().index(self)
diff --git a/src/dsf/commands/code_parameter.py b/src/dsf/commands/code_parameter.py
index 5e2fcf2..c6f163e 100644
--- a/src/dsf/commands/code_parameter.py
+++ b/src/dsf/commands/code_parameter.py
@@ -1,13 +1,13 @@
"""
codeparameter contains all classes and methods dealing with deserialized code parameters.
"""
+
import json
from typing import Self, TypeAlias, TypedDict, cast, Optional
from ..exceptions import CodeParserException
from ..object_model.move.driver_id import DriverId
-
CodeParameterScalar: TypeAlias = str | int | float | DriverId
CodeParameterArray: TypeAlias = list[int] | list[float] | list[DriverId]
CodeParameterValue: TypeAlias = CodeParameterScalar | CodeParameterArray
diff --git a/src/dsf/commands/generic.py b/src/dsf/commands/generic.py
index 7fda155..b7cd7ee 100644
--- a/src/dsf/commands/generic.py
+++ b/src/dsf/commands/generic.py
@@ -48,8 +48,9 @@ def flush(channel: CodeChannel, sync_file_streams: bool = False, if_executing: b
:returns: true if the flush request is successful
"""
- return BaseCommand("Flush",
- **{"channel": channel, "syncFileStreams": sync_file_streams, "ifExecuting": if_executing})
+ return BaseCommand(
+ "Flush", **{"channel": channel, "syncFileStreams": sync_file_streams, "ifExecuting": if_executing}
+ )
def invalidate_channel(channel: CodeChannel):
diff --git a/src/dsf/commands/plugins.py b/src/dsf/commands/plugins.py
index eaeb835..8a3b166 100644
--- a/src/dsf/commands/plugins.py
+++ b/src/dsf/commands/plugins.py
@@ -34,9 +34,7 @@ def set_plugin_data(plugin: str, key: str, value: object):
raise ValueError("plugin must not be empty")
if not key:
raise ValueError("key must not be empty")
- return BaseCommand(
- "SetPluginData", **{"plugin": plugin, "key": key, "value": value}
- )
+ return BaseCommand("SetPluginData", **{"plugin": plugin, "key": key, "value": value})
def start_plugin(plugin: str, save_state: bool = True):
diff --git a/src/dsf/commands/responses.py b/src/dsf/commands/responses.py
index fd59cb8..c4240cc 100644
--- a/src/dsf/commands/responses.py
+++ b/src/dsf/commands/responses.py
@@ -18,6 +18,7 @@
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see .
"""
+
from typing import Generic, TypeVar
from ..utils import JSONObj, JSONElement
@@ -27,13 +28,16 @@
class BaseResponse:
"""Base class for every response to a command request."""
+
success: bool
def __init__(self, success: bool):
self.success = success
+
class Response(BaseResponse, Generic[TResult]):
"""Response of a Command"""
+
result: TResult
def __init__(self, result: TResult):
@@ -43,6 +47,7 @@ def __init__(self, result: TResult):
class ErrorResponse(BaseResponse):
"""Response indicating a runtime exception during the internal processing of a command"""
+
error_type: str
error_message: str
diff --git a/src/dsf/connections/__init__.py b/src/dsf/connections/__init__.py
index d3f0758..3db0b57 100644
--- a/src/dsf/connections/__init__.py
+++ b/src/dsf/connections/__init__.py
@@ -51,16 +51,15 @@ class SubscriptionMode(str, Enum):
from .intercept_connection import InterceptConnection
from .subscribe_connection import SubscribeConnection
-
__all__ = [
- 'ConnectionMode',
- 'InterceptionMode',
- 'SubscriptionMode',
- 'BaseCommandConnection',
- 'BaseConnection',
- 'CommandConnection',
- 'InternalServerException',
- 'TaskCanceledException',
- 'InterceptConnection',
- 'SubscribeConnection',
+ "ConnectionMode",
+ "InterceptionMode",
+ "SubscriptionMode",
+ "BaseCommandConnection",
+ "BaseConnection",
+ "CommandConnection",
+ "InternalServerException",
+ "TaskCanceledException",
+ "InterceptConnection",
+ "SubscribeConnection",
]
diff --git a/src/dsf/connections/base_command_connection.py b/src/dsf/connections/base_command_connection.py
index b3b1179..00a74c5 100644
--- a/src/dsf/connections/base_command_connection.py
+++ b/src/dsf/connections/base_command_connection.py
@@ -144,10 +144,7 @@ def perform_code(self, cde: code.Code) -> Optional[Message]:
return res.result
def perform_simple_code(
- self,
- cde: str,
- channel: CodeChannel = CodeChannel.DEFAULT_CHANNEL,
- async_exec: bool = False
+ self, cde: str, channel: CodeChannel = CodeChannel.DEFAULT_CHANNEL, async_exec: bool = False
) -> str:
"""Execute an arbitrary G/M/T-code in text form
@@ -171,9 +168,7 @@ def reload_plugin(self, plugin: str):
def remove_http_endpoint(self, endpoint_type: HttpEndpointType, namespace: str, path: str):
"""Remove an existing HTTP endpoint"""
- res = self.perform_command(
- commands.http_endpoints.remove_http_endpoint(endpoint_type, namespace, path)
- )
+ res = self.perform_command(commands.http_endpoints.remove_http_endpoint(endpoint_type, namespace, path))
return res.result
def remove_user_session(self, session_id: int):
@@ -267,7 +262,5 @@ def write_message(
log_level: LogLevel,
):
"""Write an arbitrary message"""
- res = self.perform_command(
- commands.generic.write_message(message_type, message, output_message, log_level)
- )
+ res = self.perform_command(commands.generic.write_message(message_type, message, output_message, log_level))
return res.result
diff --git a/src/dsf/connections/base_connection.py b/src/dsf/connections/base_connection.py
index 50a4911..b817ad4 100644
--- a/src/dsf/connections/base_connection.py
+++ b/src/dsf/connections/base_connection.py
@@ -15,8 +15,7 @@
class _JSONDeserializable(Protocol):
@classmethod
- def from_json(cls, data: Any) -> Self:
- ...
+ def from_json(cls, data: Any) -> Self: ...
TDeserializable = TypeVar("TDeserializable", bound=_JSONDeserializable)
@@ -98,9 +97,7 @@ def perform_command(
if response.error_type == "TaskCanceledException":
raise TaskCanceledException(response.error_message)
- raise InternalServerException(
- command, response.error_type, response.error_message
- )
+ raise InternalServerException(command, response.error_type, response.error_message)
def send(self, msg: object):
"""Serialize an arbitrary object into JSON and send it to the server plus NL"""
diff --git a/src/dsf/connections/exceptions.py b/src/dsf/connections/exceptions.py
index 7bac18a..0c68d5f 100644
--- a/src/dsf/connections/exceptions.py
+++ b/src/dsf/connections/exceptions.py
@@ -1,5 +1,6 @@
from ..commands.base_command import BaseCommand
+
class IncompatibleVersionException(Exception):
"""Exception raised if the API version of the client is incompatible to the server"""
diff --git a/src/dsf/connections/init_messages/__init__.py b/src/dsf/connections/init_messages/__init__.py
index 610b490..1a85361 100644
--- a/src/dsf/connections/init_messages/__init__.py
+++ b/src/dsf/connections/init_messages/__init__.py
@@ -1,4 +1,3 @@
from . import client_init_messages, server_init_message
-
-__all__ = ['client_init_messages', 'server_init_message']
+__all__ = ["client_init_messages", "server_init_message"]
diff --git a/src/dsf/connections/init_messages/client_init_messages.py b/src/dsf/connections/init_messages/client_init_messages.py
index dfba6f8..e6371bd 100644
--- a/src/dsf/connections/init_messages/client_init_messages.py
+++ b/src/dsf/connections/init_messages/client_init_messages.py
@@ -17,6 +17,7 @@
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see .
"""
+
from typing import List, Any
from .server_init_message import ServerInitMessage
from .. import ConnectionMode, InterceptionMode, SubscriptionMode
@@ -37,12 +38,13 @@ def __init__(self, mode: ConnectionMode = ConnectionMode.UNKNOWN, **kwargs: Any)
def intercept_init_message(
- intercept_mode: InterceptionMode,
- channels: List[CodeChannel],
- filters: List[str],
- priority_codes: bool,
- auto_flush: bool = True,
- auto_evaluate_expression: bool = True) -> ClientInitMessage:
+ intercept_mode: InterceptionMode,
+ channels: List[CodeChannel],
+ filters: List[str],
+ priority_codes: bool,
+ auto_flush: bool = True,
+ auto_evaluate_expression: bool = True,
+) -> ClientInitMessage:
"""
Enter interception mode
Whenever a code is received, the connection must respond with one of
@@ -85,15 +87,17 @@ def command_init_message() -> ClientInitMessage:
return ClientInitMessage(ConnectionMode.COMMAND)
-def subscribe_init_message(subscription_mode: SubscriptionMode, filter_list: List[str] = [], verbose: bool = False, obsolete: bool = False) -> ClientInitMessage:
+def subscribe_init_message(
+ subscription_mode: SubscriptionMode, filter_list: List[str] = [], verbose: bool = False, obsolete: bool = False
+) -> ClientInitMessage:
"""_summary_
Args:
subscription_mode (SubscriptionMode): FULL to receive the entire OM every request, PATCH to only get the changes
- filter_list (List[str], optional):
+ filter_list (List[str], optional):
The style of a filter is similar to XPath. For example, if you want to monitor only the current heater
temperatures, you can use the filter expression "heat/heaters[*]/current". Wildcards are supported either
- for full names or indices. To get updates for an entire namespace, the ** wildcard can be used
+ for full names or indices. To get updates for an entire namespace, the ** wildcard can be used
for example heat/** for everything heat-related), however it can be only used at the end of a filter expression.
Defaults to [].
verbose (bool): whether object model fields flagged as verbose are required.
diff --git a/src/dsf/connections/init_messages/server_init_message.py b/src/dsf/connections/init_messages/server_init_message.py
index 814d36b..70806ca 100644
--- a/src/dsf/connections/init_messages/server_init_message.py
+++ b/src/dsf/connections/init_messages/server_init_message.py
@@ -29,7 +29,7 @@ class ServerInitMessage:
"""
@classmethod
- def from_json(cls, data: JSONObj) -> 'ServerInitMessage':
+ def from_json(cls, data: JSONObj) -> "ServerInitMessage":
"""Deserialize a dictionary coming from JSON into an instance of this class"""
version = data.get("version")
id = data.get("id")
diff --git a/src/dsf/connections/intercept_connection.py b/src/dsf/connections/intercept_connection.py
index 94e55e5..c0126c8 100644
--- a/src/dsf/connections/intercept_connection.py
+++ b/src/dsf/connections/intercept_connection.py
@@ -37,7 +37,7 @@ def __init__(
auto_evaluate_expression: bool = True,
priority_codes: bool = False,
debug: bool = False,
- timeout: int = 0
+ timeout: int = 0,
):
super().__init__(debug, timeout)
self.interception_mode = interception_mode
@@ -55,7 +55,7 @@ def connect(self, socket_file: str = SOCKET_FILE): # noqa
self.filters,
self.priority_codes,
self.auto_flush,
- self.auto_evaluate_expression
+ self.auto_evaluate_expression,
)
return super()._connect(iim, socket_file)
diff --git a/src/dsf/connections/subscribe_connection.py b/src/dsf/connections/subscribe_connection.py
index b6cd4ee..32f37e7 100644
--- a/src/dsf/connections/subscribe_connection.py
+++ b/src/dsf/connections/subscribe_connection.py
@@ -9,15 +9,13 @@
from ..object_model import ObjectModel
from ..utils import JSONObj
-
_MISSING = object()
class KeySubscriptionCallback(Protocol):
"""Callback invoked for a subscribed object model key"""
- def __call__(self, *, key: str, data: Any, indices: tuple[int, ...] | None) -> None:
- ...
+ def __call__(self, *, key: str, data: Any, indices: tuple[int, ...] | None) -> None: ...
@dataclass(frozen=True)
@@ -74,13 +72,16 @@ def get_object_model(self) -> ObjectModel:
Later calls apply every queued patch without blocking, update the
cached object model, and run any registered key callbacks synchronously.
"""
- if (self.subscription_mode == client_init_messages.SubscriptionMode.FULL or not self._initial_object_model_received):
+ if (
+ self.subscription_mode == client_init_messages.SubscriptionMode.FULL
+ or not self._initial_object_model_received
+ ):
self._object_model = self.receive(ObjectModel)
self._initial_object_model_received = True
self.send(commands.model_subscription.acknowledge())
return self._object_model
else:
- while (self.has_data_available()):
+ while self.has_data_available():
patch_json = self.get_object_model_patch()
patch_data = json.loads(patch_json)
self._object_model.update_from_json(patch_data)
@@ -178,10 +179,7 @@ def _extract_key_path_values(
key: str,
) -> list[tuple[tuple[int, ...] | None, Any]]:
matches = cls._walk_key_path(patch_data, key.split("."), ())
- return [
- (indexes if indexes else None, value)
- for indexes, value in matches
- ]
+ return [(indexes if indexes else None, value) for indexes, value in matches]
@classmethod
def _walk_key_path(
diff --git a/src/dsf/http.py b/src/dsf/http.py
index 1d01271..c724490 100644
--- a/src/dsf/http.py
+++ b/src/dsf/http.py
@@ -14,6 +14,7 @@
HttpCallback: TypeAlias = Callable[["HttpEndpointConnection"], Coroutine[Any, Any, None]]
+
class HttpResponseType(str, Enum):
"""Enumeration of supported HTTP responses"""
@@ -43,7 +44,9 @@ def from_json(cls, data: JSONObj) -> "ReceivedHttpRequest":
body=body,
)
- def __init__(self, sessionId: int, queries: dict[str, str], headers: dict[str, str], contentType: Optional[str], body: str):
+ def __init__(
+ self, sessionId: int, queries: dict[str, str], headers: dict[str, str], contentType: Optional[str], body: str
+ ):
self.session_id = sessionId
self.queries = queries
self.headers = headers
@@ -54,7 +57,9 @@ def __init__(self, sessionId: int, queries: dict[str, str], headers: dict[str, s
class HttpEndpointConnection:
"""Connection class for dealing with requests received from a custom HTTP endpoint"""
- def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, is_websocket: bool, debug: bool = False):
+ def __init__(
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, is_websocket: bool, debug: bool = False
+ ):
"""Constructor for a new connection dealing with a single HTTP endpoint request"""
self.reader = reader
self.writer = writer
@@ -172,7 +177,7 @@ def _create_socket(self, path: str):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
# Check for abstract socket. `str` and `bytes` paths are supported.
- if path[0] not in (0, '\x00'):
+ if path[0] not in (0, "\x00"):
try:
if stat.S_ISSOCK(os.stat(path).st_mode):
os.remove(path)
@@ -192,7 +197,7 @@ def _create_socket(self, path: str):
if exc.errno == errno.EADDRINUSE:
# Let's improve the error message by adding
# with what exact address it occurs.
- msg = f'Address {path!r} is already in use'
+ msg = f"Address {path!r} is already in use"
raise OSError(errno.EADDRINUSE, msg) from None
else:
raise
@@ -208,9 +213,7 @@ def start_connection_listener(self):
asyncio.set_event_loop(self._loop)
sock = self._create_socket(self.socket_file)
self._server = self._loop.run_until_complete(
- asyncio.start_unix_server(
- self.handle_connection, sock=sock, backlog=self.backlog
- )
+ asyncio.start_unix_server(self.handle_connection, sock=sock, backlog=self.backlog)
)
self._loop.run_forever()
finally:
diff --git a/src/dsf/object_model/__init__.py b/src/dsf/object_model/__init__.py
index db52d94..9f98aa3 100644
--- a/src/dsf/object_model/__init__.py
+++ b/src/dsf/object_model/__init__.py
@@ -13,69 +13,79 @@
from .plugins import Plugin, PluginManifest, SbcPermissions
from .sbc import CPU, Memory, SBC, Upgrade
from .sbc.dsf import AccessLevel, HttpEndpoint, HttpEndpointType, SessionType, UserSession
-from .sensors import Accelerometer, AnalogSensor, AnalogSensorType, Endstop, EndstopType, GpInputPort, Probe, ProbeLoadCell, ProbeType, Sensors
+from .sensors import (
+ Accelerometer,
+ AnalogSensor,
+ AnalogSensorType,
+ Endstop,
+ EndstopType,
+ GpInputPort,
+ Probe,
+ ProbeLoadCell,
+ ProbeType,
+ Sensors,
+)
from .spindles import Spindle, SpindleState
from .state import LogLevel, MachineStatus, MessageBox, MessageBoxMode, State
from .tools import Tool, ToolState
from .volumes import Volume
-
__all__ = [
- 'Board',
- 'Boards',
- 'BoardState',
- 'ExpansionBoard',
- 'MainBoard',
- 'Directories',
- 'Fan',
- 'Heat',
- 'Heater',
- 'HeaterState',
- 'InputChannel',
- 'Job',
- 'LedStrip',
- 'LedStripType',
- 'Limits',
- 'Message',
- 'MessageType',
- 'DriverId',
- 'Move',
- 'Network',
- 'NetworkInterface',
- 'NetworkInterfaceType',
- 'NetworkProtocol',
- 'NetworkState',
- 'ObjectModel',
- 'Plugin',
- 'PluginManifest',
- 'SbcPermissions',
- 'CPU',
- 'Memory',
- 'SBC',
- 'Upgrade',
- 'AccessLevel',
- 'HttpEndpoint',
- 'HttpEndpointType',
- 'SessionType',
- 'UserSession',
- 'Accelerometer',
- 'AnalogSensor',
- 'AnalogSensorType',
- 'Endstop',
- 'EndstopType',
- 'GpInputPort',
- 'Probe',
- 'ProbeLoadCell',
- 'ProbeType',
- 'Sensors',
- 'Spindle',
- 'SpindleState',
- 'LogLevel',
- 'MachineStatus',
- 'MessageBox',
- 'MessageBoxMode',
- 'State',
- 'Tool',
- 'ToolState',
- 'Volume',
+ "Board",
+ "Boards",
+ "BoardState",
+ "ExpansionBoard",
+ "MainBoard",
+ "Directories",
+ "Fan",
+ "Heat",
+ "Heater",
+ "HeaterState",
+ "InputChannel",
+ "Job",
+ "LedStrip",
+ "LedStripType",
+ "Limits",
+ "Message",
+ "MessageType",
+ "DriverId",
+ "Move",
+ "Network",
+ "NetworkInterface",
+ "NetworkInterfaceType",
+ "NetworkProtocol",
+ "NetworkState",
+ "ObjectModel",
+ "Plugin",
+ "PluginManifest",
+ "SbcPermissions",
+ "CPU",
+ "Memory",
+ "SBC",
+ "Upgrade",
+ "AccessLevel",
+ "HttpEndpoint",
+ "HttpEndpointType",
+ "SessionType",
+ "UserSession",
+ "Accelerometer",
+ "AnalogSensor",
+ "AnalogSensorType",
+ "Endstop",
+ "EndstopType",
+ "GpInputPort",
+ "Probe",
+ "ProbeLoadCell",
+ "ProbeType",
+ "Sensors",
+ "Spindle",
+ "SpindleState",
+ "LogLevel",
+ "MachineStatus",
+ "MessageBox",
+ "MessageBoxMode",
+ "State",
+ "Tool",
+ "ToolState",
+ "Volume",
]
diff --git a/src/dsf/object_model/boards/__init__.py b/src/dsf/object_model/boards/__init__.py
index 051980f..ae95b7c 100644
--- a/src/dsf/object_model/boards/__init__.py
+++ b/src/dsf/object_model/boards/__init__.py
@@ -5,5 +5,17 @@
from .driver_closed_loop import DriverClosedLoop, ClosedLoopCurrentFraction, ClosedLoopPositionError
from .min_max_current import MinMaxCurrent
-__all__ = ['Board', 'Boards', 'BoardState', 'ExpansionBoard', 'MainBoard', 'BoardClosedLoop', 'DirectDisplay', 'Driver',
- 'DriverClosedLoop', 'ClosedLoopCurrentFraction', 'ClosedLoopPositionError', 'MinMaxCurrent']
+__all__ = [
+ "Board",
+ "Boards",
+ "BoardState",
+ "ExpansionBoard",
+ "MainBoard",
+ "BoardClosedLoop",
+ "DirectDisplay",
+ "Driver",
+ "DriverClosedLoop",
+ "ClosedLoopCurrentFraction",
+ "ClosedLoopPositionError",
+ "MinMaxCurrent",
+]
diff --git a/src/dsf/object_model/boards/boards.py b/src/dsf/object_model/boards/boards.py
index 8879d76..8a13b88 100644
--- a/src/dsf/object_model/boards/boards.py
+++ b/src/dsf/object_model/boards/boards.py
@@ -42,43 +42,43 @@ class Board(ModelObject):
"""
# CAN address of this board or None if not applicable
- can_address = nullable_model_prop('can_address', int)
+ can_address = nullable_model_prop("can_address", int)
# Drivers of this board
- drivers = nullable_model_prop('drivers', ModelCollection[Driver], lambda: ModelCollection(Driver))
+ drivers = nullable_model_prop("drivers", ModelCollection[Driver], lambda: ModelCollection(Driver))
# Date of the firmware build
- firmware_date = model_prop('firmware_date', str, "")
+ firmware_date = model_prop("firmware_date", str, "")
# Filename of the firmware binary
- firmware_file_name = model_prop('firmware_file_name', str, "")
+ firmware_file_name = model_prop("firmware_file_name", str, "")
# Version of the firmware build
- firmware_version = model_prop('firmware_version', str, "")
+ firmware_version = model_prop("firmware_version", str, "")
# Amount of free RAM on this board (in bytes or null if unknown)
- free_ram = nullable_model_prop('free_ram', int)
+ free_ram = nullable_model_prop("free_ram", int)
# Maximum number of motors this board can drive
- max_motors = model_prop('max_motors', int)
+ max_motors = model_prop("max_motors", int)
# Minimum, maximum, and current temperatures of the MCU or None if unknown
- mcu_temp = nullable_model_prop('mcu_temp', MinMaxCurrent)
+ mcu_temp = nullable_model_prop("mcu_temp", MinMaxCurrent)
# Full name of the board
- name = model_prop('name', str, "")
+ name = model_prop("name", str, "")
# Short name of the board
- short_name = model_prop('short_name', str, "")
+ short_name = model_prop("short_name", str, "")
# Unique identifier of the board or None if unknown
- unique_id = nullable_model_prop('unique_id', str)
+ unique_id = nullable_model_prop("unique_id", str)
# Minimum, maximum, and current voltages on the 12V rail or None if unknown
- v_12 = nullable_model_prop('v_12', MinMaxCurrent)
+ v_12 = nullable_model_prop("v_12", MinMaxCurrent)
# Minimum, maximum, and current voltages on the input rail or None if unknown
- v_in = nullable_model_prop('v_in', MinMaxCurrent)
+ v_in = nullable_model_prop("v_in", MinMaxCurrent)
def __init__(self):
super(Board, self).__init__()
@@ -88,25 +88,25 @@ class MainBoard(Board):
"""Information about the mainboard, which is always the first item of the boards list"""
# Details about a connected display or None if none is connected
- direct_display = nullable_model_prop('direct_display', DirectDisplay)
+ direct_display = nullable_model_prop("direct_display", DirectDisplay)
# Name of the firmware build
- firmware_name = model_prop('firmware_name', str, "")
+ firmware_name = model_prop("firmware_name", str, "")
# Filename of the IAP binary that is used for updates from the SBC or None if unsupported
- iap_file_name_SBC = nullable_model_prop('iap_file_name_SBC', str)
+ iap_file_name_SBC = nullable_model_prop("iap_file_name_SBC", str)
# Filename of the IAP binary that is used for updates from the SD card or None if unsupported
- iap_file_name_SD = nullable_model_prop('iap_file_name_SD', str)
+ iap_file_name_SD = nullable_model_prop("iap_file_name_SD", str)
# Maximum number of heaters this board can control
- max_heaters = model_prop('max_heaters', int)
+ max_heaters = model_prop("max_heaters", int)
# Indicates if this board supports external displays
- supports_direct_display = model_prop('supports_direct_display', bool, False)
+ supports_direct_display = model_prop("supports_direct_display", bool, False)
# Filename of the on-board WiFi chip or None if not present
- wifi_firmware_file_name = nullable_model_prop('wifi_firmware_file_name', str)
+ wifi_firmware_file_name = nullable_model_prop("wifi_firmware_file_name", str)
def __init__(self):
super(MainBoard, self).__init__()
@@ -116,16 +116,16 @@ class ExpansionBoard(Board):
"""Information about an expansion board connected over CAN"""
# Closed loop data of this board or None if unknown
- closed_loop = nullable_model_prop('closed_loop', BoardClosedLoop)
+ closed_loop = nullable_model_prop("closed_loop", BoardClosedLoop)
# Information about an inductive sensor or None if not present
- inductive_sensor = nullable_model_prop('inductive_sensor', InductiveSensor)
+ inductive_sensor = nullable_model_prop("inductive_sensor", InductiveSensor)
# State of this board
- state = model_prop('state', BoardState, BoardState.unknown)
+ state = model_prop("state", BoardState, BoardState.unknown)
# Connection timeout of this board (in s)
- timeout = model_prop('timeout', int, 10)
+ timeout = model_prop("timeout", int, 10)
def __init__(self):
super(ExpansionBoard, self).__init__()
diff --git a/src/dsf/object_model/boards/direct_display/__init__.py b/src/dsf/object_model/boards/direct_display/__init__.py
index b1f904d..da56744 100644
--- a/src/dsf/object_model/boards/direct_display/__init__.py
+++ b/src/dsf/object_model/boards/direct_display/__init__.py
@@ -4,5 +4,10 @@
from .direct_display_screen import DirectDisplayScreen
from .direct_display_screen_st7567 import DirectDisplayScreenST7567
-__all__ = ['DirectDisplay', 'DirectDisplayController', 'DirectDisplayEncoder',
- 'DirectDisplayScreen', 'DirectDisplayScreenST7567']
+__all__ = [
+ "DirectDisplay",
+ "DirectDisplayController",
+ "DirectDisplayEncoder",
+ "DirectDisplayScreen",
+ "DirectDisplayScreenST7567",
+]
diff --git a/src/dsf/object_model/boards/direct_display/direct_display.py b/src/dsf/object_model/boards/direct_display/direct_display.py
index f520557..19fd2e2 100644
--- a/src/dsf/object_model/boards/direct_display/direct_display.py
+++ b/src/dsf/object_model/boards/direct_display/direct_display.py
@@ -9,10 +9,10 @@ class DirectDisplay(ModelObject):
"""Class providing information about a connected direct-connect display"""
# Encoder of this screen or null if none
- encoder = nullable_model_prop('encoder', DirectDisplayEncoder)
-
+ encoder = nullable_model_prop("encoder", DirectDisplayEncoder)
+
# Screen information
- screen = model_prop('screen', DirectDisplayScreen, DirectDisplayScreen())
+ screen = model_prop("screen", DirectDisplayScreen, DirectDisplayScreen())
def __init__(self):
super(DirectDisplay, self).__init__()
diff --git a/src/dsf/object_model/boards/direct_display/direct_display_screen.py b/src/dsf/object_model/boards/direct_display/direct_display_screen.py
index 933ab7b..772e0cb 100644
--- a/src/dsf/object_model/boards/direct_display/direct_display_screen.py
+++ b/src/dsf/object_model/boards/direct_display/direct_display_screen.py
@@ -8,20 +8,20 @@ class DirectDisplayScreen(ModelObject):
"""Class providing information about a connected display screen"""
# Number of colour bits
- colour_bits = model_prop("colour_bits",int, 1)
-
+ colour_bits = model_prop("colour_bits", int, 1)
+
# Display type
controller = model_prop("controller", DirectDisplayController, DirectDisplayController.ST7920)
-
+
# Height of the display screen in pixels
- height = model_prop("height",int, 64)
-
+ height = model_prop("height", int, 64)
+
# SPI frequency of the display (in Hz)
- spi_freq = model_prop("spi_freq",int, 0)
-
+ spi_freq = model_prop("spi_freq", int, 0)
+
# Width of the display screen in pixels
- width = model_prop("width",int, 128)
-
+ width = model_prop("width", int, 128)
+
def __init__(self, controller: DirectDisplayController = DirectDisplayController.ST7920):
super().__init__()
@@ -43,8 +43,8 @@ def get_direct_display_screen_type(type_: DirectDisplayController):
def _update_from_json(self, **kwargs: JSONElement):
"""Override ObjectModel._update_from_json
to return the DirectDisplayScreen type matching the given controller"""
- if 'controller' in kwargs:
- controller = DirectDisplayController(kwargs.get('controller'))
+ if "controller" in kwargs:
+ controller = DirectDisplayController(kwargs.get("controller"))
if controller != self.controller:
required_type = self.get_direct_display_screen_type(controller)
new_direct_display_screen = required_type.update_from_json(kwargs)
diff --git a/src/dsf/object_model/boards/driver.py b/src/dsf/object_model/boards/driver.py
index 236fe96..848e09e 100644
--- a/src/dsf/object_model/boards/driver.py
+++ b/src/dsf/object_model/boards/driver.py
@@ -8,10 +8,10 @@ class Driver(ModelObject):
"""Information about a driver"""
# Closed-loop settings (if applicable)
- closed_loop = nullable_model_prop('closed_loop', DriverClosedLoop)
+ closed_loop = nullable_model_prop("closed_loop", DriverClosedLoop)
# Configured (M569) settings of this driver
- config = model_prop('config', DriverConfig)
+ config = model_prop("config", DriverConfig)
# Driver status register value
status = model_prop("status", int)
diff --git a/src/dsf/object_model/boards/driver_closed_loop.py b/src/dsf/object_model/boards/driver_closed_loop.py
index a7fcaf8..b16d307 100644
--- a/src/dsf/object_model/boards/driver_closed_loop.py
+++ b/src/dsf/object_model/boards/driver_closed_loop.py
@@ -30,7 +30,7 @@ def __init__(self):
class DriverClosedLoop(ModelObject):
"""This represents information about closed-loop tuning"""
-
+
# Current fraction of the configured motor current used
current_fraction = model_prop("current_fraction", ClosedLoopCurrentFraction)
diff --git a/src/dsf/object_model/boards/driver_config.py b/src/dsf/object_model/boards/driver_config.py
index a28abdc..e57895b 100644
--- a/src/dsf/object_model/boards/driver_config.py
+++ b/src/dsf/object_model/boards/driver_config.py
@@ -7,10 +7,10 @@ class DriverConfig(ModelObject):
"""Configured (M569) settings of a driver"""
# Configured direction of the driver (false = reverse, true = forward)
- direction = model_prop('direction', bool, True)
+ direction = model_prop("direction", bool, True)
# Configured driver mode (only available for smart drivers)
- mode = nullable_model_prop('mode', DriverMode, lambda: None)
+ mode = nullable_model_prop("mode", DriverMode, lambda: None)
def __init__(self):
super(DriverConfig, self).__init__()
diff --git a/src/dsf/object_model/boards/driver_mode.py b/src/dsf/object_model/boards/driver_mode.py
index 2176f66..f102715 100644
--- a/src/dsf/object_model/boards/driver_mode.py
+++ b/src/dsf/object_model/boards/driver_mode.py
@@ -5,22 +5,22 @@ class DriverMode(int, Enum):
"""State of a channel"""
# Constant off-time chopper
- constantOffTime = 0,
+ constantOffTime = (0,)
# Random off-time chopper
- randomOffTime = 1,
+ randomOffTime = (1,)
# SpreadCycle
- spreadCycle = 2,
+ spreadCycle = (2,)
# StealthChop (includes stealthChop2)
- stealthChop = 3,
+ stealthChop = (3,)
# Field-oriented control (direct)
- direct = 4,
+ direct = (4,)
# Assisted open loop
- assistedOpen = 5,
+ assistedOpen = (5,)
# Driver mode is unknown
unknown = 6
diff --git a/src/dsf/object_model/boards/min_max_current.py b/src/dsf/object_model/boards/min_max_current.py
index 288ea4d..c489005 100644
--- a/src/dsf/object_model/boards/min_max_current.py
+++ b/src/dsf/object_model/boards/min_max_current.py
@@ -1,12 +1,13 @@
from ..model_object import ModelObject
from ..utils import model_prop
+
class MinMaxCurrent(ModelObject):
"""Provides minimum, maximum and current values"""
# Current value
current = model_prop("current", float)
-
+
# Minimum value
min = model_prop("min", float)
@@ -15,4 +16,3 @@ class MinMaxCurrent(ModelObject):
def __init__(self):
super(MinMaxCurrent, self).__init__()
-
\ No newline at end of file
diff --git a/src/dsf/object_model/directories/__init__.py b/src/dsf/object_model/directories/__init__.py
index e3bdbb7..6186f3f 100644
--- a/src/dsf/object_model/directories/__init__.py
+++ b/src/dsf/object_model/directories/__init__.py
@@ -1,3 +1,3 @@
from .directories import *
-__all__ = ['Directories']
+__all__ = ["Directories"]
diff --git a/src/dsf/object_model/fans/__init__.py b/src/dsf/object_model/fans/__init__.py
index 437207f..af0a0d1 100644
--- a/src/dsf/object_model/fans/__init__.py
+++ b/src/dsf/object_model/fans/__init__.py
@@ -1,4 +1,4 @@
from .fans import Fan
from .fan_thermostatic_control import FanThermostaticControl
-__all__ = ['Fan', 'FanThermostaticControl']
+__all__ = ["Fan", "FanThermostaticControl"]
diff --git a/src/dsf/object_model/heat/__init__.py b/src/dsf/object_model/heat/__init__.py
index aa666a7..d70a360 100644
--- a/src/dsf/object_model/heat/__init__.py
+++ b/src/dsf/object_model/heat/__init__.py
@@ -4,5 +4,13 @@
from .heater_model_pid import HeaterModelPID
from .heater_monitor import HeaterMonitor, HeaterMonitorAction, HeaterMonitorCondition
-__all__ = ['Heat', 'Heater', 'HeaterState', 'HeaterModel', 'HeaterModelPID', 'HeaterMonitor', 'HeaterMonitorAction',
- 'HeaterMonitorCondition']
+__all__ = [
+ "Heat",
+ "Heater",
+ "HeaterState",
+ "HeaterModel",
+ "HeaterModelPID",
+ "HeaterMonitor",
+ "HeaterMonitorAction",
+ "HeaterMonitorCondition",
+]
diff --git a/src/dsf/object_model/heat/heat.py b/src/dsf/object_model/heat/heat.py
index 6cdbcc4..c9f3726 100644
--- a/src/dsf/object_model/heat/heat.py
+++ b/src/dsf/object_model/heat/heat.py
@@ -21,7 +21,9 @@ class Heat(ModelObject):
chamber_heaters = model_prop("chamber_heaters", ModelCollection[int], ModelCollection(int))
# List of configured chamber heaters (indices), grouped by chamber (first dimension)
- chamber_heater_mapping = model_prop("chamber_heater_mapping", ModelCollection[list[int]], ModelCollection(list[int]))
+ chamber_heater_mapping = model_prop(
+ "chamber_heater_mapping", ModelCollection[list[int]], ModelCollection(list[int])
+ )
# Minimum required temperature for extrusion moves (in C)
cold_extrude_temperature = model_prop("cold_extrude_temperature", float, 160)
diff --git a/src/dsf/object_model/inputs/__init__.py b/src/dsf/object_model/inputs/__init__.py
index e7abdd5..3924085 100644
--- a/src/dsf/object_model/inputs/__init__.py
+++ b/src/dsf/object_model/inputs/__init__.py
@@ -4,4 +4,4 @@
from .input_channel import InputChannel
from .input_channel_state import InputChannelState
-__all__ = ['Compatibility', 'DistanceUnit', 'Inputs', 'InputChannel', 'InputChannelState']
+__all__ = ["Compatibility", "DistanceUnit", "Inputs", "InputChannel", "InputChannelState"]
diff --git a/src/dsf/object_model/job/__init__.py b/src/dsf/object_model/job/__init__.py
index fab2216..e7df336 100644
--- a/src/dsf/object_model/job/__init__.py
+++ b/src/dsf/object_model/job/__init__.py
@@ -6,5 +6,4 @@
from .thumbnail_info import ThumbnailInfo, ThumbnailInfoFormat
from .times_left import TimesLeft
-
-__all__ = ['Build', 'BuildObject', 'GCodeFileInfo', 'Job', 'Layer', 'ThumbnailInfo', 'ThumbnailInfoFormat', 'TimesLeft']
+__all__ = ["Build", "BuildObject", "GCodeFileInfo", "Job", "Layer", "ThumbnailInfo", "ThumbnailInfoFormat", "TimesLeft"]
diff --git a/src/dsf/object_model/job/job.py b/src/dsf/object_model/job/job.py
index 968f249..5f7bc95 100644
--- a/src/dsf/object_model/job/job.py
+++ b/src/dsf/object_model/job/job.py
@@ -11,39 +11,39 @@ class Job(ModelObject):
"""Information about the current job"""
# Information about the current build or None if not available
- build = nullable_model_prop('build', Build)
+ build = nullable_model_prop("build", Build)
# Total active duration of the current job file (in s or None)
- duration = nullable_model_prop('duration', int)
+ duration = nullable_model_prop("duration", int)
# Information about the file being processed
- file = model_prop('file', GCodeFileInfo)
+ file = model_prop("file", GCodeFileInfo)
# Current position in the file being processed (in bytes or None)
- file_position = nullable_model_prop('file_position', int)
+ file_position = nullable_model_prop("file_position", int)
# Total duration of the last job (in s or None)
- last_duration = nullable_model_prop('last_duration', int)
+ last_duration = nullable_model_prop("last_duration", int)
# Name of the last file processed or None
- last_file_name = nullable_model_prop('last_file_name', str)
+ last_file_name = nullable_model_prop("last_file_name", str)
# Indicates if the last file was aborted (unexpected cancellation)
- last_file_aborted = model_prop('last_file_aborted', bool, False)
+ last_file_aborted = model_prop("last_file_aborted", bool, False)
# Indicates if the last file was cancelled (user cancelled)
- last_file_cancelled = model_prop('last_file_cancelled', bool, False)
+ last_file_cancelled = model_prop("last_file_cancelled", bool, False)
# Indicates if the last file processed was simulated
- last_file_simulated = model_prop('last_file_simulated', bool, False)
+ last_file_simulated = model_prop("last_file_simulated", bool, False)
# Warm-up duration of the last print or None if not available (in s)
- last_warm_up_duration = nullable_model_prop('last_warm_up_duration', int)
+ last_warm_up_duration = nullable_model_prop("last_warm_up_duration", int)
# Number of the current layer or None if not available
- layer = nullable_model_prop('layer', int)
+ layer = nullable_model_prop("layer", int)
# Information about the past layers
- layers = model_prop('layers', ModelCollection[Layer], ModelCollection(Layer))
+ layers = model_prop("layers", ModelCollection[Layer], ModelCollection(Layer))
# Time elapsed since the last layer change (in s or None)
- layer_time = nullable_model_prop('layer_time', float)
+ layer_time = nullable_model_prop("layer_time", float)
# Total pause time since the job started
- pause_duration = nullable_model_prop('pause_duration', int)
+ pause_duration = nullable_model_prop("pause_duration", int)
# Total extrusion amount without extrusion factors applied (in mm)
- raw_extrusion = nullable_model_prop('raw_extrusion', float)
+ raw_extrusion = nullable_model_prop("raw_extrusion", float)
# Estimated times left
- times_left = model_prop('times_left', TimesLeft)
+ times_left = model_prop("times_left", TimesLeft)
# Time needed to heat up the heaters (in s or None)
- warm_up_duration = nullable_model_prop('warm_up_duration', int)
+ warm_up_duration = nullable_model_prop("warm_up_duration", int)
def __init__(self):
super().__init__()
diff --git a/src/dsf/object_model/led_strips/__init__.py b/src/dsf/object_model/led_strips/__init__.py
index 4395e7d..9654f7b 100644
--- a/src/dsf/object_model/led_strips/__init__.py
+++ b/src/dsf/object_model/led_strips/__init__.py
@@ -1,4 +1,3 @@
from .led_strip import LedStrip, LedStripType
-
-__all__ = ['LedStrip', 'LedStripType']
+__all__ = ["LedStrip", "LedStripType"]
diff --git a/src/dsf/object_model/led_strips/led_strip.py b/src/dsf/object_model/led_strips/led_strip.py
index ea72d6e..a3c6c0c 100644
--- a/src/dsf/object_model/led_strips/led_strip.py
+++ b/src/dsf/object_model/led_strips/led_strip.py
@@ -23,22 +23,22 @@ class LedStrip(ModelObject):
"""Type of this LED strip"""
# Board address of the corresponding pin
- board = model_prop('board', int, 0)
+ board = model_prop("board", int, 0)
# Order in which colour components are sent to the strip
- color_order = model_prop('color_order', LedStripColorOrder, LedStripColorOrder.BGR)
+ color_order = model_prop("color_order", LedStripColorOrder, LedStripColorOrder.BGR)
# Maximum number of LEDs that can be addressed on this strip
- max_leds = model_prop('max_leds', int, 0)
+ max_leds = model_prop("max_leds", int, 0)
# Name of the pin this LED strip is connected to
- pin = model_prop('pin', str, "")
+ pin = model_prop("pin", str, "")
# Indicates if this strip is bit-banged and therefore requires motion to be stopped before sending a command
- stop_movement = model_prop('stop_movement', bool, False)
+ stop_movement = model_prop("stop_movement", bool, False)
# Type of this LED strip
- type = model_prop('type', LedStripType, LedStripType.DotStar)
+ type = model_prop("type", LedStripType, LedStripType.DotStar)
def __init__(self):
super().__init__()
diff --git a/src/dsf/object_model/led_strips/led_strip_color_order.py b/src/dsf/object_model/led_strips/led_strip_color_order.py
index 57334ed..abbd7b5 100644
--- a/src/dsf/object_model/led_strips/led_strip_color_order.py
+++ b/src/dsf/object_model/led_strips/led_strip_color_order.py
@@ -1,22 +1,23 @@
from enum import Enum
+
class LedStripColorOrder(int, Enum):
"""Color order of the LED strip"""
# Default order for DotStar LEDs
- BGR = 0,
+ BGR = (0,)
# Blue, red, green
- BRG = 1,
+ BRG = (1,)
# Red, green, blue
- RGB = 2,
+ RGB = (2,)
# Red, blue, green
- RBG = 3,
+ RBG = (3,)
# Green, blue, red
- GBR = 4,
+ GBR = (4,)
# Default order for WS2812 (NeoPixel) LEDs
- GRB = 5
\ No newline at end of file
+ GRB = 5
diff --git a/src/dsf/object_model/limits/__init__.py b/src/dsf/object_model/limits/__init__.py
index 49b461d..3487401 100644
--- a/src/dsf/object_model/limits/__init__.py
+++ b/src/dsf/object_model/limits/__init__.py
@@ -1,3 +1,3 @@
from .limits import Limits
-__all__ = ['Limits']
+__all__ = ["Limits"]
diff --git a/src/dsf/object_model/messages/__init__.py b/src/dsf/object_model/messages/__init__.py
index c7c15a0..d4c3838 100644
--- a/src/dsf/object_model/messages/__init__.py
+++ b/src/dsf/object_model/messages/__init__.py
@@ -1,3 +1,3 @@
from .messages import Message, MessageType
-__all__ = ['Message', 'MessageType']
+__all__ = ["Message", "MessageType"]
diff --git a/src/dsf/object_model/model_collection.py b/src/dsf/object_model/model_collection.py
index 31544ae..057bddd 100644
--- a/src/dsf/object_model/model_collection.py
+++ b/src/dsf/object_model/model_collection.py
@@ -3,7 +3,7 @@
from ..utils import JSONElement
from .model_type import ModelType
-T = TypeVar('T')
+T = TypeVar("T")
# T = TypeVar('T', bound=Union[JSONElement, "ModelCollection[Any]", ModelDictionary, ModelObject])
@@ -12,7 +12,7 @@ class ModelCollection(ModelType[list[JSONElement]], Generic[T], list[T]):
Class for storing model object items in a list
Useful for updating model object items from JSON data (patches)
"""
-
+
def __init__(
self,
item_constructor: type[T] | object,
@@ -30,10 +30,12 @@ def __init__(
self._declared_item_constructor = item_constructor
item_origin = get_origin(item_constructor)
item_args = get_args(item_constructor)
- self._allow_none = allow_none or (item_origin in (Union, getattr(__import__('types'), 'UnionType', Union)) and type(None) in item_args)
+ self._allow_none = allow_none or (
+ item_origin in (Union, getattr(__import__("types"), "UnionType", Union)) and type(None) in item_args
+ )
resolved_constructor: object = item_constructor
- if item_origin in (Union, getattr(__import__('types'), 'UnionType', Union)):
+ if item_origin in (Union, getattr(__import__("types"), "UnionType", Union)):
non_none_args = [arg for arg in item_args if arg is not type(None)]
if len(non_none_args) == 1:
resolved_constructor = non_none_args[0]
@@ -50,7 +52,7 @@ def __init__(
if value is not None:
self[:] = []
- for (index, item) in enumerate(value):
+ for index, item in enumerate(value):
if item is None:
self.append(self._coerce_item_value(item))
continue
@@ -60,8 +62,10 @@ def __init__(
else:
ref_item = self._create_item(index)
if not is_model_object(ref_item):
- raise TypeError(f"Item constructor for ModelCollection must inherit from type ModelType to update from a dict."
- f" Got {type(ref_item).__name__}: {ref_item}")
+ raise TypeError(
+ f"Item constructor for ModelCollection must inherit from type ModelType to update from a dict."
+ f" Got {type(ref_item).__name__}: {ref_item}"
+ )
# if issubclass(self._item_constructor, ModelType[T]):
ref_item.update_from_json(item)
@@ -90,14 +94,16 @@ def _coerce_item_value(self, value: JSONElement) -> T:
try:
return self._item_constructor(value)
except (TypeError, ValueError, KeyError):
- raise ValueError(f"Invalid enum value {value} for collection of type {self._runtime_model_type.__name__}")
+ raise ValueError(
+ f"Invalid enum value {value} for collection of type {self._runtime_model_type.__name__}"
+ )
try:
return self._item_constructor(value)
except (TypeError, ValueError):
return cast(T, value)
- def update_from_json(self, data: list[JSONElement]) -> 'ModelCollection[T]':
+ def update_from_json(self, data: list[JSONElement]) -> "ModelCollection[T]":
"""
Update this instance from the given data
:param json_element: JSON data to upgrade this instance from
@@ -110,13 +116,13 @@ def update_from_json(self, data: list[JSONElement]) -> 'ModelCollection[T]':
raise Exception(f"Invalid JSON element type for model collection {type(data)}.")
# Remove deleted items
- self[:] = self[:len(data)]
+ self[:] = self[: len(data)]
# Update existing items
for i in range(0, min(len(self), len(data))):
current_item = self[i]
new_item_data = data[i]
-
+
# If the new item data is null, set the current item to null (even if it was a model object before)
if new_item_data is None and self._allow_none:
self[i] = cast(T, None)
@@ -162,5 +168,4 @@ def update_from_json(self, data: list[JSONElement]) -> 'ModelCollection[T]':
else:
self.append(self._coerce_item_value(item_to_add))
-
return self
diff --git a/src/dsf/object_model/model_dictionary.py b/src/dsf/object_model/model_dictionary.py
index fbbf6c3..9019343 100644
--- a/src/dsf/object_model/model_dictionary.py
+++ b/src/dsf/object_model/model_dictionary.py
@@ -44,8 +44,10 @@ def __setitem__(self, key: str, value: Any) -> None:
current_item = self.get(key)
if current_item is None and self._item_constructor:
if not isinstance(value, dict):
- raise TypeError(f"Value for key '{key}' must be of type dict to update the model object."
- f" Got {type(value)}: {value}")
+ raise TypeError(
+ f"Value for key '{key}' must be of type dict to update the model object."
+ f" Got {type(value)}: {value}"
+ )
new_item = self._item_constructor()
return super().__setitem__(key, new_item.update_from_json(cast(JSONObj, value)))
elif is_model_object(current_item):
diff --git a/src/dsf/object_model/model_object.py b/src/dsf/object_model/model_object.py
index 5091942..bd9f218 100644
--- a/src/dsf/object_model/model_object.py
+++ b/src/dsf/object_model/model_object.py
@@ -9,7 +9,7 @@
class FloatJSON(float):
# Remove trailing zeros from float numbers
def __repr__(self) -> str:
- return f'{self:g}'
+ return f"{self:g}"
_encoder = cast(Any, json.encoder)
@@ -38,7 +38,7 @@ def __json_serialize(obj: object) -> str | JSONObj:
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, float):
- return f'{obj:g}'
+ return f"{obj:g}"
if isinstance(obj, SbcPermissions):
return obj.name
if isinstance(obj, DriverId):
@@ -46,7 +46,7 @@ def __json_serialize(obj: object) -> str | JSONObj:
# Convert snake_case class attributes into CamelCase JSON style
# also convert back 'globals' to 'global'
- return {snake_to_camel(str(k) if k != '_globals' else '_global'): v for k, v in obj.__dict__.items()}
+ return {snake_to_camel(str(k) if k != "_globals" else "_global"): v for k, v in obj.__dict__.items()}
def _update_from_json(self: TModelObject, **kwargs: JSONElement) -> TModelObject:
"""Update this instance from a given JSON element
@@ -58,13 +58,14 @@ def _update_from_json(self: TModelObject, **kwargs: JSONElement) -> TModelObject
# Get the class writeable properties including from inherited classes
# (the ones which have a setter -> fset property object attribute)
cls_dict = {attr: getattr(self.__class__, attr) for attr in dir(self.__class__)}
- writeable_properties = [attr for attr, value in cls_dict.items()
- if isinstance(value, property) and value.fset is not None]
+ writeable_properties = [
+ attr for attr, value in cls_dict.items() if isinstance(value, property) and value.fset is not None
+ ]
instance_attributes = vars(self)
for json_key, json_value in kwargs.items():
# Convert JSON attributes from CamelCase to snake_case to satisfy python PEP8 naming
# Remove trailing underscore set by preserve_builtin()
- json_key_snake = camel_to_snake(json_key.rstrip('_'))
+ json_key_snake = camel_to_snake(json_key.rstrip("_"))
# Write public attributes by using their setter property
if json_key_snake in writeable_properties:
attr = getattr(self, json_key_snake)
@@ -102,4 +103,3 @@ def update_from_json(self: TModelObject, data: Union[JSONObj, str]) -> TModelObj
def to_json(self) -> str:
"""Serialize this instance of this class into a JSON dictionary"""
return self.__str__()
-
diff --git a/src/dsf/object_model/model_type.py b/src/dsf/object_model/model_type.py
index 5d5b6ce..7766226 100644
--- a/src/dsf/object_model/model_type.py
+++ b/src/dsf/object_model/model_type.py
@@ -5,6 +5,7 @@
T = TypeVar("T", bound=JSONElement, contravariant=True)
TModelValue: TypeAlias = "ModelType[T]"
+
@runtime_checkable
class ModelType(Protocol[T]):
@classmethod
diff --git a/src/dsf/object_model/move/__init__.py b/src/dsf/object_model/move/__init__.py
index ca2d1e7..206bb0c 100644
--- a/src/dsf/object_model/move/__init__.py
+++ b/src/dsf/object_model/move/__init__.py
@@ -16,26 +16,25 @@
from .probe_grid import ProbeGrid
from .skew import Skew
-
__all__ = [
- 'Axis',
- 'AxisLetter',
- 'CurrentMove',
- 'DriverId',
- 'Extruder',
- 'ExtruderNonlinear',
- 'InputShaping',
- 'InputShapingType',
- 'MicroStepping',
- 'MotorsIdleControl',
- 'Move',
- 'MoveCalibration',
- 'MoveCompensation',
- 'MoveCompensationType',
- 'MoveDeviations',
- 'MoveQueueItem',
- 'MoveRotation',
- 'MoveSegmentation',
- 'ProbeGrid',
- 'Skew',
+ "Axis",
+ "AxisLetter",
+ "CurrentMove",
+ "DriverId",
+ "Extruder",
+ "ExtruderNonlinear",
+ "InputShaping",
+ "InputShapingType",
+ "MicroStepping",
+ "MotorsIdleControl",
+ "Move",
+ "MoveCalibration",
+ "MoveCompensation",
+ "MoveCompensationType",
+ "MoveDeviations",
+ "MoveQueueItem",
+ "MoveRotation",
+ "MoveSegmentation",
+ "ProbeGrid",
+ "Skew",
]
diff --git a/src/dsf/object_model/move/axis.py b/src/dsf/object_model/move/axis.py
index aabbbcb..a6b6a05 100644
--- a/src/dsf/object_model/move/axis.py
+++ b/src/dsf/object_model/move/axis.py
@@ -10,43 +10,43 @@
class AxisLetter(str, Enum):
"""List of supported axis letters"""
- X = 'X'
- Y = 'Y'
- Z = 'Z'
- U = 'U'
- V = 'V'
- W = 'W'
- A = 'A'
- B = 'B'
- C = 'C'
- D = 'D'
- a = 'a'
- b = 'b'
- c = 'c'
- d = 'd'
- e = 'e'
- f = 'f'
- g = 'g'
- h = 'h'
- i = 'i'
- j = 'j'
- k = 'k'
- l = 'l'
- m = 'm'
- n = 'n'
- o = 'o'
- p = 'p'
- q = 'q'
- r = 'r'
- s = 's'
- t = 't'
- u = 'u'
- v = 'v'
- w = 'w'
- x = 'x'
- y = 'y'
- z = 'z'
- none = ''
+ X = "X"
+ Y = "Y"
+ Z = "Z"
+ U = "U"
+ V = "V"
+ W = "W"
+ A = "A"
+ B = "B"
+ C = "C"
+ D = "D"
+ a = "a"
+ b = "b"
+ c = "c"
+ d = "d"
+ e = "e"
+ f = "f"
+ g = "g"
+ h = "h"
+ i = "i"
+ j = "j"
+ k = "k"
+ l = "l"
+ m = "m"
+ n = "n"
+ o = "o"
+ p = "p"
+ q = "q"
+ r = "r"
+ s = "s"
+ t = "t"
+ u = "u"
+ v = "v"
+ w = "w"
+ x = "x"
+ y = "y"
+ z = "z"
+ none = ""
class Axis(ModelObject):
@@ -128,4 +128,4 @@ class Axis(ModelObject):
workplace_offsets = model_prop("workplace_offsets", ModelCollection[float], ModelCollection(float))
def __init__(self):
- super().__init__()
\ No newline at end of file
+ super().__init__()
diff --git a/src/dsf/object_model/move/driver_id.py b/src/dsf/object_model/move/driver_id.py
index db3983c..fa2deff 100644
--- a/src/dsf/object_model/move/driver_id.py
+++ b/src/dsf/object_model/move/driver_id.py
@@ -21,7 +21,13 @@ class DriverId(ModelObject):
board: int
port: int
- def __init__(self, as_str: Optional[str] = None, as_int: Optional[int] = None, board: Optional[int] = None, port: Optional[int] = None):
+ def __init__(
+ self,
+ as_str: Optional[str] = None,
+ as_int: Optional[int] = None,
+ board: Optional[int] = None,
+ port: Optional[int] = None,
+ ):
super().__init__()
self.board = board if board is not None else 0
@@ -67,7 +73,7 @@ def __hash__(self) -> int:
def update_from_json(self, data: JSONObj | str) -> Self:
if not isinstance(data, str):
raise TypeError(f"DriverId must be updated from a string. Got {type(data).__name__}: {data}")
- matches = re.search(r'(\d+)\.(\d+)', data)
+ matches = re.search(r"(\d+)\.(\d+)", data)
if matches:
self.board = int(matches.group(1))
self.port = int(matches.group(2))
@@ -75,4 +81,3 @@ def update_from_json(self, data: JSONObj | str) -> Self:
self.board = 0
self.port = int(data)
return self
-
diff --git a/src/dsf/object_model/move/extruder.py b/src/dsf/object_model/move/extruder.py
index ef3ed5c..deeca15 100644
--- a/src/dsf/object_model/move/extruder.py
+++ b/src/dsf/object_model/move/extruder.py
@@ -16,7 +16,7 @@ class Extruder(ModelObject):
current = model_prop("current", int, 0)
# Assigned driver
- driver = nullable_model_prop('driver', DriverId)
+ driver = nullable_model_prop("driver", DriverId)
# Extrusion factor to use (0..1 or greater)
factor = model_prop("factor", float, 1.0)
@@ -49,11 +49,11 @@ class Extruder(ModelObject):
position = model_prop("position", float, 0.0)
# Pressure advance parameters (see M572)
- press_adv = model_prop('press_adv', ExtruderPressureAdvance)
+ press_adv = model_prop("press_adv", ExtruderPressureAdvance)
# Pressure advance
# Obsolete: use press_adv.k0 instead
- pressure_advance = model_prop('pressure_advance', float, 0)
+ pressure_advance = model_prop("pressure_advance", float, 0)
# Motor jerk during the current print only (in mm/s)
printing_jerk = model_prop("printing_jerk", float, 15.0)
diff --git a/src/dsf/object_model/move/extruder_pressure_advance.py b/src/dsf/object_model/move/extruder_pressure_advance.py
index 8bb0196..74c02fb 100644
--- a/src/dsf/object_model/move/extruder_pressure_advance.py
+++ b/src/dsf/object_model/move/extruder_pressure_advance.py
@@ -13,6 +13,6 @@ class ExtruderPressureAdvance(ModelObject):
# K1 coeffient
k1 = model_prop("k1", float, 0.0)
-
+
def __init__(self):
super().__init__()
diff --git a/src/dsf/object_model/move/input_shaping.py b/src/dsf/object_model/move/input_shaping.py
index 07bb9c0..e9129d1 100644
--- a/src/dsf/object_model/move/input_shaping.py
+++ b/src/dsf/object_model/move/input_shaping.py
@@ -44,7 +44,7 @@ def _missing_(cls, value: object):
class InputShaping(ModelObject):
- """Parameters describing input shaping """
+ """Parameters describing input shaping"""
# Amplitudes of the input shaper
amplitudes = model_prop("amplitudes", ModelCollection[float], ModelCollection(float))
@@ -68,4 +68,4 @@ class InputShaping(ModelObject):
type = model_prop("type", InputShapingType, InputShapingType.none)
def __init__(self):
- super().__init__()
\ No newline at end of file
+ super().__init__()
diff --git a/src/dsf/object_model/move/kinematics/__init__.py b/src/dsf/object_model/move/kinematics/__init__.py
index 4f4adc0..240c24e 100644
--- a/src/dsf/object_model/move/kinematics/__init__.py
+++ b/src/dsf/object_model/move/kinematics/__init__.py
@@ -9,5 +9,15 @@
from .tilt_correction import TiltCorrection
from .zleadscrew_kinematics import ZLeadscrewKinematics
-__all__ = ["CoreKinematics", "DeltaKinematics", "DeltaTower", "HangprinterKinematics", "Kinematics",
- "KinematicsName", "PolarKinematics", "ScaraKinematics", "TiltCorrection", "ZLeadscrewKinematics"]
+__all__ = [
+ "CoreKinematics",
+ "DeltaKinematics",
+ "DeltaTower",
+ "HangprinterKinematics",
+ "Kinematics",
+ "KinematicsName",
+ "PolarKinematics",
+ "ScaraKinematics",
+ "TiltCorrection",
+ "ZLeadscrewKinematics",
+]
diff --git a/src/dsf/object_model/move/kinematics/core_kinematics.py b/src/dsf/object_model/move/kinematics/core_kinematics.py
index 06bcb29..7535b7d 100644
--- a/src/dsf/object_model/move/kinematics/core_kinematics.py
+++ b/src/dsf/object_model/move/kinematics/core_kinematics.py
@@ -5,16 +5,8 @@
class CoreKinematics(ZLeadscrewKinematics):
- forward_matrix = model_prop("forward_matrix", list[list[float]], [
- [1, 0, 0],
- [0, 1, 0],
- [0, 0, 1]
- ])
- inverse_matrix = model_prop("inverse_matrix", list[list[float]], [
- [1, 0, 0],
- [0, 1, 0],
- [0, 0, 1]
- ])
+ forward_matrix = model_prop("forward_matrix", list[list[float]], [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
+ inverse_matrix = model_prop("inverse_matrix", list[list[float]], [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
def __init__(self, name: KinematicsName = KinematicsName.cartesian):
super(CoreKinematics, self).__init__(name)
diff --git a/src/dsf/object_model/move/kinematics/hangprinter_kinematics.py b/src/dsf/object_model/move/kinematics/hangprinter_kinematics.py
index c651ab8..09b6a91 100644
--- a/src/dsf/object_model/move/kinematics/hangprinter_kinematics.py
+++ b/src/dsf/object_model/move/kinematics/hangprinter_kinematics.py
@@ -6,15 +6,11 @@
class HangprinterKinematics(Kinematics):
"""Information about hangprinter kinematics"""
- anchors = model_prop("anchors", list[list[float]], [
- [0, -2000, -100],
- [2000, 1000, -100],
- [-2000, 1000, -100],
- [0, 0, 3000]
- ])
+ anchors = model_prop(
+ "anchors", list[list[float]], [[0, -2000, -100], [2000, 1000, -100], [-2000, 1000, -100], [0, 0, 3000]]
+ )
print_radius = model_prop("print_radius", float, 1500)
def __init__(self):
super(HangprinterKinematics, self).__init__()
self.name = KinematicsName.hangprinter
-
diff --git a/src/dsf/object_model/move/kinematics/kinematics.py b/src/dsf/object_model/move/kinematics/kinematics.py
index 3fba4e7..9ca78e8 100644
--- a/src/dsf/object_model/move/kinematics/kinematics.py
+++ b/src/dsf/object_model/move/kinematics/kinematics.py
@@ -29,7 +29,7 @@ def get_kinematics_type(name: KinematicsName | str) -> "Kinematics":
from .polar_kinematics import PolarKinematics
from .scara_kinematics import ScaraKinematics
- name = KinematicsName(name.lower().replace(' ', ''))
+ name = KinematicsName(name.lower().replace(" ", ""))
if name in [
KinematicsName.cartesian,
@@ -37,7 +37,7 @@ def get_kinematics_type(name: KinematicsName | str) -> "Kinematics":
KinematicsName.coreXYU,
KinematicsName.coreXYUV,
KinematicsName.coreXZ,
- KinematicsName.markForged
+ KinematicsName.markForged,
]:
return CoreKinematics(name)
elif name == KinematicsName.linearDelta:
@@ -54,10 +54,10 @@ def get_kinematics_type(name: KinematicsName | str) -> "Kinematics":
def _update_from_json(self, **kwargs: JSONElement) -> "Kinematics":
"""Override ObjectModel._update_from_json to return the Kinematics type matching the given name"""
- name = kwargs.get('name')
+ name = kwargs.get("name")
if isinstance(name, str):
- kinematics_name = KinematicsName(name.lower().replace(' ', ''))
- kwargs['name'] = kinematics_name
+ kinematics_name = KinematicsName(name.lower().replace(" ", ""))
+ kwargs["name"] = kinematics_name
if self.name != kinematics_name:
return self.get_kinematics_type(kinematics_name).update_from_json(kwargs)
diff --git a/src/dsf/object_model/move/kinematics/polar_kinematics.py b/src/dsf/object_model/move/kinematics/polar_kinematics.py
index c704109..34d9dba 100644
--- a/src/dsf/object_model/move/kinematics/polar_kinematics.py
+++ b/src/dsf/object_model/move/kinematics/polar_kinematics.py
@@ -11,7 +11,7 @@ class PolarKinematics(Kinematics):
# Homed radius (in mm)
radius_homed = model_prop("radius_homed", float, 0.0)
- #Maximum radius (in mm)
+ # Maximum radius (in mm)
radius_max = model_prop("radius_max", float, 0.0)
# Minimum radius (in mm)
@@ -26,4 +26,3 @@ class PolarKinematics(Kinematics):
def __init__(self):
super(PolarKinematics, self).__init__()
self._name = KinematicsName.polar
-
diff --git a/src/dsf/object_model/move/kinematics/scara_kinematics.py b/src/dsf/object_model/move/kinematics/scara_kinematics.py
index d08e4c9..339bbe4 100644
--- a/src/dsf/object_model/move/kinematics/scara_kinematics.py
+++ b/src/dsf/object_model/move/kinematics/scara_kinematics.py
@@ -35,4 +35,3 @@ class ScaraKinematics(ZLeadscrewKinematics):
def __init__(self, name: KinematicsName):
super(ScaraKinematics, self).__init__(name)
-
diff --git a/src/dsf/object_model/move/motion_system.py b/src/dsf/object_model/move/motion_system.py
index aa5ddf8..c992419 100644
--- a/src/dsf/object_model/move/motion_system.py
+++ b/src/dsf/object_model/move/motion_system.py
@@ -6,6 +6,7 @@
from .move_rotation import MoveRotation
from ..state.restore_point import RestorePoint
+
class MotionSystem(ModelObject):
"""Information about a motion system"""
diff --git a/src/dsf/object_model/move/move.py b/src/dsf/object_model/move/move.py
index 0e685fd..e7a74b6 100644
--- a/src/dsf/object_model/move/move.py
+++ b/src/dsf/object_model/move/move.py
@@ -19,79 +19,79 @@ class Move(ModelObject):
"""Information about the move subsystem"""
# Value of the M201 T parameter. Only present in builds that support S-curve acceleration
- acceleration_time = nullable_model_prop('acceleration_time', float)
+ acceleration_time = nullable_model_prop("acceleration_time", float)
# List of the configured axes
- axes = model_prop('axes', ModelCollection[Axis], ModelCollection(Axis))
+ axes = model_prop("axes", ModelCollection[Axis], ModelCollection(Axis))
# Backlash distance multiplier
- backlash_factor = model_prop('backlash_factor', int, 10)
+ backlash_factor = model_prop("backlash_factor", int, 10)
# Information about the automatic calibration
- calibration = model_prop('calibration', MoveCalibration)
+ calibration = model_prop("calibration", MoveCalibration)
# Information about the currently configured compensation options
- compensation = model_prop('compensation', MoveCompensation)
+ compensation = model_prop("compensation", MoveCompensation)
# Information about the current move
- current_move = model_prop('current_move', CurrentMove)
+ current_move = model_prop("current_move", CurrentMove)
# List of configured extruders
- extruders = model_prop('extruders', ModelCollection[Extruder], ModelCollection(Extruder))
+ extruders = model_prop("extruders", ModelCollection[Extruder], ModelCollection(Extruder))
# Idle current reduction parameters
- idle = model_prop('idle', MotorsIdleControl)
+ idle = model_prop("idle", MotorsIdleControl)
# List of configured keep-out zones
- keepout = model_prop('keepout', ModelCollection[KeepoutZone], ModelCollection(KeepoutZone))
+ keepout = model_prop("keepout", ModelCollection[KeepoutZone], ModelCollection(KeepoutZone))
# Configured kinematics options
- kinematics = model_prop('kinematics', Kinematics)
+ kinematics = model_prop("kinematics", Kinematics)
# Limit axis positions by their minima and maxima
- limit_axes = model_prop('limit_axes', bool, True)
+ limit_axes = model_prop("limit_axes", bool, True)
# Minimum allowed movement speed (in mm/min)
- min_speed = model_prop('min_speed', float, 30)
+ min_speed = model_prop("min_speed", float, 30)
# Indicates if standard moves are forbidden if the corresponding axis is not homed
- no_moves_before_homing = model_prop('no_moves_before_homing', bool, True)
+ no_moves_before_homing = model_prop("no_moves_before_homing", bool, True)
# List of configured motion systems
- motion_systems = model_prop('motion_systems', ModelCollection[MotionSystem], ModelCollection(MotionSystem))
+ motion_systems = model_prop("motion_systems", ModelCollection[MotionSystem], ModelCollection(MotionSystem))
# Maximum acceleration allowed while printing (in mm/s^2)
# deprecated, use motion_systems[].printing_acceleration instead
- printing_acceleration = model_prop('printing_acceleration', float, 10000)
+ printing_acceleration = model_prop("printing_acceleration", float, 10000)
# List of move queue items (DDA rings)
- queue = model_prop('queue', ModelCollection[MoveQueueItem], ModelCollection(MoveQueueItem))
+ queue = model_prop("queue", ModelCollection[MoveQueueItem], ModelCollection(MoveQueueItem))
# Parameters for centre rotation
# deprecated, use motion_systems[].rotation instead
- rotation = model_prop('rotation', MoveRotation)
+ rotation = model_prop("rotation", MoveRotation)
# Parameters for input shaping
- shaping = model_prop('shaping', InputShaping)
+ shaping = model_prop("shaping", InputShaping)
# Speed factor applied to every regular move (0.01..1 or greater)
- speed_factor = model_prop('speed_factor', float, 1)
+ speed_factor = model_prop("speed_factor", float, 1)
# Maximum acceleration allowed while travelling (in mm/s^2)
# deprecated, use motion_systems[].travel_acceleration instead
- travel_acceleration = model_prop('travel_acceleration', float, 10000)
+ travel_acceleration = model_prop("travel_acceleration", float, 10000)
# Indicates if third-order S-curve acceleration is enabled.
# Only present in builds that support S-curve acceleration
- using_S_curve = model_prop('using_S_curve', bool, False)
+ using_S_curve = model_prop("using_S_curve", bool, False)
# Virtual total extruder position
# deprecated, use motion_systems[].virtual_e_pos instead
- virtual_e_pos = model_prop('virtual_e_pos', float, 0)
+ virtual_e_pos = model_prop("virtual_e_pos", float, 0)
# Index of the currently selected workplace
# deprecated, use motion_systems[].workplace_number instead
- workplace_number = model_prop('workplace_number', int, 0)
+ workplace_number = model_prop("workplace_number", int, 0)
def __init__(self):
- super().__init__()
\ No newline at end of file
+ super().__init__()
diff --git a/src/dsf/object_model/move/move_compensation.py b/src/dsf/object_model/move/move_compensation.py
index 6cc6389..a21b8df 100644
--- a/src/dsf/object_model/move/move_compensation.py
+++ b/src/dsf/object_model/move/move_compensation.py
@@ -21,25 +21,25 @@ class MoveCompensation(ModelObject):
"""Information about the configured compensation options"""
# Effective height before the bed compensation is turned off (in mm) or null if not configured
- fade_height = nullable_model_prop('fade_height', float)
-
+ fade_height = nullable_model_prop("fade_height", float)
+
# Full path to the currently used height map file or null if none is in use
- file = nullable_model_prop('file', str)
+ file = nullable_model_prop("file", str)
# Grid settings of the loaded heightmap or null if no heightmap is loaded
- live_grid = nullable_model_prop('live_grid', ProbeGrid)
+ live_grid = nullable_model_prop("live_grid", ProbeGrid)
# Deviations of the mesh grid or null if not applicable
- mesh_deviation = nullable_model_prop('mesh_deviation', MoveDeviations)
+ mesh_deviation = nullable_model_prop("mesh_deviation", MoveDeviations)
# Probe grid settings as defined by M557
- probe_grid = model_prop('probe_grid', ProbeGrid)
+ probe_grid = model_prop("probe_grid", ProbeGrid)
# Information about the configured orthogonal axis parameters
- skew = model_prop('skew', Skew)
+ skew = model_prop("skew", Skew)
# Type of the compensation in use
- type = model_prop('type', MoveCompensationType, MoveCompensationType.none)
+ type = model_prop("type", MoveCompensationType, MoveCompensationType.none)
def __init__(self):
super().__init__()
diff --git a/src/dsf/object_model/move/probe_grid.py b/src/dsf/object_model/move/probe_grid.py
index 20d84d4..8d660ff 100644
--- a/src/dsf/object_model/move/probe_grid.py
+++ b/src/dsf/object_model/move/probe_grid.py
@@ -7,7 +7,7 @@ class ProbeGrid(ModelObject):
"""Information about the configured probe grid (see M557)"""
# Axis letters of this heightmap
- axes = model_prop("axes", ModelCollection[str], ModelCollection(str, ['X', 'Y']))
+ axes = model_prop("axes", ModelCollection[str], ModelCollection(str, ["X", "Y"]))
# End coordinates of the heightmap
maxs = model_prop("maxs", ModelCollection[float], ModelCollection(float, [-1.0, -1.0]))
diff --git a/src/dsf/object_model/network/__init__.py b/src/dsf/object_model/network/__init__.py
index 05c50dc..51259dd 100644
--- a/src/dsf/object_model/network/__init__.py
+++ b/src/dsf/object_model/network/__init__.py
@@ -4,4 +4,4 @@
from .network_protocol import NetworkProtocol
from .network_state import NetworkState
-__all__ = ['Network', 'NetworkInterface', 'NetworkInterfaceType', 'NetworkProtocol', 'NetworkState']
+__all__ = ["Network", "NetworkInterface", "NetworkInterfaceType", "NetworkProtocol", "NetworkState"]
diff --git a/src/dsf/object_model/network/network_interface.py b/src/dsf/object_model/network/network_interface.py
index e0f33fc..83d9c9b 100644
--- a/src/dsf/object_model/network/network_interface.py
+++ b/src/dsf/object_model/network/network_interface.py
@@ -10,45 +10,47 @@ class NetworkInterface(ModelObject):
"""Information about a network interface"""
# List of active protocols
- active_protocols = model_prop('active_protocols', ModelCollection[NetworkProtocol], ModelCollection(NetworkProtocol))
+ active_protocols = model_prop(
+ "active_protocols", ModelCollection[NetworkProtocol], ModelCollection(NetworkProtocol)
+ )
# Actual IPv4 address of the network adapter or null if unknown
- actual_IP = nullable_model_prop('actual_IP', str)
-
+ actual_IP = nullable_model_prop("actual_IP", str)
+
# Configured IPv4 address of the network adapter or null if unknown
- configured_IP = nullable_model_prop('configured_IP', str)
-
+ configured_IP = nullable_model_prop("configured_IP", str)
+
# Configured IPv4 DNS server fo the network adapter or null if unknown
- dns_server = nullable_model_prop('dns_server', str)
-
- firmware_version = nullable_model_prop('firmware_version', str)
+ dns_server = nullable_model_prop("dns_server", str)
+
+ firmware_version = nullable_model_prop("firmware_version", str)
# IPv4 gateway of the network adapter or null if unknown
- gateway = nullable_model_prop('gateway', str)
+ gateway = nullable_model_prop("gateway", str)
# Physical address of the network adapter or null if unknown
- mac = nullable_model_prop('mac', str)
+ mac = nullable_model_prop("mac", str)
# Received signal strength indicator of the WiFi adapter (only WiFi, in dBm, or null if unknown)
- rssi = nullable_model_prop('rssi', int)
+ rssi = nullable_model_prop("rssi", int)
# Speed of the network interface (in MBit, null if unknown, 0 if not connected)
- speed = nullable_model_prop('speed', int)
+ speed = nullable_model_prop("speed", int)
# SSID of the Wi-Fi network or null if not applicable
- ssid = nullable_model_prop('ssid', str)
+ ssid = nullable_model_prop("ssid", str)
# State of this network interface or null if unknown
- state = nullable_model_prop('state', NetworkState, constructor=lambda: None)
+ state = nullable_model_prop("state", NetworkState, constructor=lambda: None)
# Subnet of the network adapter or null if unknown
- subnet = nullable_model_prop('subnet', str)
+ subnet = nullable_model_prop("subnet", str)
# Type of this network interface
- type = model_prop('type', NetworkInterfaceType, NetworkInterfaceType.wifi)
+ type = model_prop("type", NetworkInterfaceType, NetworkInterfaceType.wifi)
# WiFi country code if this is a WiFi adapter and if the country code can be determined
- wifi_country = nullable_model_prop('wifi_country', str)
+ wifi_country = nullable_model_prop("wifi_country", str)
def __init__(self):
super().__init__()
diff --git a/src/dsf/object_model/object_model.py b/src/dsf/object_model/object_model.py
index a3751db..36d710f 100644
--- a/src/dsf/object_model/object_model.py
+++ b/src/dsf/object_model/object_model.py
@@ -30,36 +30,35 @@ class ObjectModel(ModelObject):
# Information about the SBC which Duet Software Framework is running on.
# This is None if the system is operating in standalone mode
- boards = model_prop('boards', Boards, Boards())
- directories = model_prop('directories', Directories)
- fans = model_prop('fans', ModelCollection[Optional[Fan]], ModelCollection(Optional[Fan]))
- globals = model_prop('globals', ModelDictionary, ModelDictionary(False))
- heat = model_prop('heat', Heat)
- inputs = model_prop('inputs', Inputs)
- job = model_prop('job', Job)
- led_strips = model_prop('led_strips', ModelCollection[LedStrip], ModelCollection(LedStrip))
- limits = model_prop('limits', Limits)
- messages = model_prop('messages', ModelCollection[Message], ModelCollection(Message))
- move = model_prop('move', Move)
- network = model_prop('network', Network)
- plugins = model_prop('plugins', ModelDictionary, ModelDictionary(True, Plugin))
- sbc = nullable_model_prop('sbc', SBC)
- sensors = model_prop('sensors', Sensors)
- spindles = model_prop('spindles', ModelCollection[Optional[Spindle]], ModelCollection(Optional[Spindle]))
- state = model_prop('state', State)
- tools = model_prop('tools', ModelCollection[Optional[Tool]], ModelCollection(Optional[Tool]))
- volumes = model_prop('volumes', ModelCollection[Volume], ModelCollection(Volume))
-
+ boards = model_prop("boards", Boards, Boards())
+ directories = model_prop("directories", Directories)
+ fans = model_prop("fans", ModelCollection[Optional[Fan]], ModelCollection(Optional[Fan]))
+ globals = model_prop("globals", ModelDictionary, ModelDictionary(False))
+ heat = model_prop("heat", Heat)
+ inputs = model_prop("inputs", Inputs)
+ job = model_prop("job", Job)
+ led_strips = model_prop("led_strips", ModelCollection[LedStrip], ModelCollection(LedStrip))
+ limits = model_prop("limits", Limits)
+ messages = model_prop("messages", ModelCollection[Message], ModelCollection(Message))
+ move = model_prop("move", Move)
+ network = model_prop("network", Network)
+ plugins = model_prop("plugins", ModelDictionary, ModelDictionary(True, Plugin))
+ sbc = nullable_model_prop("sbc", SBC)
+ sensors = model_prop("sensors", Sensors)
+ spindles = model_prop("spindles", ModelCollection[Optional[Spindle]], ModelCollection(Optional[Spindle]))
+ state = model_prop("state", State)
+ tools = model_prop("tools", ModelCollection[Optional[Tool]], ModelCollection(Optional[Tool]))
+ volumes = model_prop("volumes", ModelCollection[Volume], ModelCollection(Volume))
def __init__(self):
super(ObjectModel, self).__init__()
- def _update_from_json(self, **kwargs: JSONElement) -> 'ObjectModel':
+ def _update_from_json(self, **kwargs: JSONElement) -> "ObjectModel":
super(ObjectModel, self)._update_from_json(**kwargs)
# "global" is a reserved keyword in Python, so it is converted to "globals"
- if 'global_' in kwargs:
- global_ = kwargs['global_']
+ if "global_" in kwargs:
+ global_ = kwargs["global_"]
if global_ is not None and not isinstance(global_, dict):
raise TypeError(f"global must be a dict or None. Got {type(global_).__name__}: {global_}")
self.globals.update_from_json(global_)
diff --git a/src/dsf/object_model/plugins/__init__.py b/src/dsf/object_model/plugins/__init__.py
index 0c8cc68..9bdba62 100644
--- a/src/dsf/object_model/plugins/__init__.py
+++ b/src/dsf/object_model/plugins/__init__.py
@@ -2,5 +2,4 @@
from .plugin_manifest import PluginManifest
from .sbc_permissions import SbcPermissions
-
-__all__ = ['Plugin', 'PluginManifest', 'SbcPermissions']
+__all__ = ["Plugin", "PluginManifest", "SbcPermissions"]
diff --git a/src/dsf/object_model/plugins/plugin_manifest.py b/src/dsf/object_model/plugins/plugin_manifest.py
index b90ed2c..aee4a08 100644
--- a/src/dsf/object_model/plugins/plugin_manifest.py
+++ b/src/dsf/object_model/plugins/plugin_manifest.py
@@ -11,77 +11,77 @@ class PluginManifest(ModelObject):
"""Information about a third-party plugin"""
# Identify of this plugin. May consist of letters and digits only (max length 32 chars)
- id = model_prop('id', str)
+ id = model_prop("id", str)
# Name of the plugin. May consist of letters, digits, dashes, and underscores only (max length 64 chars)
- name = model_prop('name', str)
+ name = model_prop("name", str)
# Author of the plugin
- author = model_prop('author', str, "")
+ author = model_prop("author", str, "")
# Version of the plugin
- version = model_prop('version', str, "1.0.0")
+ version = model_prop("version", str, "1.0.0")
# Licence of the plugin. Should follow the SPDX format (see https://spdx.org/licenses/)
- license = model_prop('license', str, "LGPL-3.0-or-later")
+ license = model_prop("license", str, "LGPL-3.0-or-later")
# Link to the plugin homepage or source code repository
- homepage = nullable_model_prop('homepage', str)
+ homepage = nullable_model_prop("homepage", str)
# List of general tags for search
- tags = model_prop('tags', ModelCollection[str], ModelCollection(str))
+ tags = model_prop("tags", ModelCollection[str], ModelCollection(str))
# Major/minor compatible DWC version
- dwc_version = nullable_model_prop('dwc_version', str)
+ dwc_version = nullable_model_prop("dwc_version", str)
# List of DWC plugins this plugin depends on. Circular dependencies are not supported
- dwc_dependencies = model_prop('dwc_dependencies', ModelCollection[str], ModelCollection(str))
+ dwc_dependencies = model_prop("dwc_dependencies", ModelCollection[str], ModelCollection(str))
# Set to true if a SBC is absolutely required for this plugin
- sbc_required = model_prop('sbc_required', bool)
+ sbc_required = model_prop("sbc_required", bool)
# Required DSF version for the plugin running on the SBC (ignored if there is no SBC executable)
- sbc_dsf_version = nullable_model_prop('sbc_dsf_version', str)
+ sbc_dsf_version = nullable_model_prop("sbc_dsf_version", str)
# Filename in the dsf directory used to start the plugin
- sbc_executable = nullable_model_prop('sbc_executable', str)
+ sbc_executable = nullable_model_prop("sbc_executable", str)
# Command-line arguments for the executable
- sbc_executable_arguments = nullable_model_prop('sbc_executable_arguments', str)
+ sbc_executable_arguments = nullable_model_prop("sbc_executable_arguments", str)
# List of other filenames in the dsf directory that should be executable
- sbc_extra_executables = model_prop('sbc_extra_executables', ModelCollection[str], ModelCollection(str))
+ sbc_extra_executables = model_prop("sbc_extra_executables", ModelCollection[str], ModelCollection(str))
# Automatically restart the SBC process when terminated
- sbc_auto_restart = model_prop('sbc_auto_restart', bool, False)
-
+ sbc_auto_restart = model_prop("sbc_auto_restart", bool, False)
+
# Plugin notifies DSF when it is fully started
- sbc_notify_started = model_prop('sbc_notify_started', bool, False)
+ sbc_notify_started = model_prop("sbc_notify_started", bool, False)
# Defines if messages from stdout/stderr are output as generic messages
- sbc_output_redirected = model_prop('sbc_output_redirected', bool, False)
+ sbc_output_redirected = model_prop("sbc_output_redirected", bool, False)
# List of permissins required by the plugin executable running on the SBC
- sbc_permissions = model_prop('sbc_permissions', ModelCollection[SbcPermissions], ModelCollection(SbcPermissions))
+ sbc_permissions = model_prop("sbc_permissions", ModelCollection[SbcPermissions], ModelCollection(SbcPermissions))
# List of files in the sys or virtual SD directory that should not be overwritten on upgrade
- sbc_config_files = model_prop('sbc_config_files', ModelCollection[str], ModelCollection(str))
+ sbc_config_files = model_prop("sbc_config_files", ModelCollection[str], ModelCollection(str))
# List of packages this plugin depends on (apt packages in the case of DuetPi)
- sbc_package_dependencies = model_prop('sbc_package_dependencies', ModelCollection[str], ModelCollection(str))
+ sbc_package_dependencies = model_prop("sbc_package_dependencies", ModelCollection[str], ModelCollection(str))
# List of Python packages this plugin depends on
- sbc_python_dependencies = model_prop('sbc_python_dependencies', ModelCollection[str], ModelCollection(str))
+ sbc_python_dependencies = model_prop("sbc_python_dependencies", ModelCollection[str], ModelCollection(str))
# List of SBC plugins this plugin depends on. Circular dependencies are not supported
- sbc_plugin_dependencies = model_prop('sbc_plugin_dependencies', ModelCollection[str], ModelCollection(str))
+ sbc_plugin_dependencies = model_prop("sbc_plugin_dependencies", ModelCollection[str], ModelCollection(str))
# Major/minor compatible RRF version
- rrf_version = nullable_model_prop('rrf_version', str)
+ rrf_version = nullable_model_prop("rrf_version", str)
# Custom plugin data to be populated in the object model (DSF/DWC in SBC mode - or - DWC in standalone mode).
- data = model_prop('data', ModelDictionary, ModelDictionary(False))
-
+ data = model_prop("data", ModelDictionary, ModelDictionary(False))
+
def __init__(self):
super(PluginManifest, self).__init__()
@@ -91,7 +91,7 @@ def check_version(actual: str, required: str):
:param actual: Actual version
:param required: Required version
:returns: Whether the actual version fulfills teh requirement"""
- split_chars = r'\.|-|\+'
+ split_chars = r"\.|-|\+"
actual_items = re.split(split_chars, actual)
required_items = re.split(split_chars, required)
for actual_idx, required_idx in zip(actual_items, required_items):
diff --git a/src/dsf/object_model/sbc/__init__.py b/src/dsf/object_model/sbc/__init__.py
index 0c2be11..0ee716d 100644
--- a/src/dsf/object_model/sbc/__init__.py
+++ b/src/dsf/object_model/sbc/__init__.py
@@ -3,5 +3,4 @@
from .sbc import SBC
from .upgrade import Upgrade
-
-__all__ = ['CPU', 'Memory', 'SBC', 'Upgrade']
+__all__ = ["CPU", "Memory", "SBC", "Upgrade"]
diff --git a/src/dsf/object_model/sbc/cpu.py b/src/dsf/object_model/sbc/cpu.py
index 166db84..4a34a09 100644
--- a/src/dsf/object_model/sbc/cpu.py
+++ b/src/dsf/object_model/sbc/cpu.py
@@ -12,4 +12,3 @@ class CPU(ModelObject):
def __init__(self):
super().__init__()
-
diff --git a/src/dsf/object_model/sbc/dsf/__init__.py b/src/dsf/object_model/sbc/dsf/__init__.py
index 243bd66..fd26813 100644
--- a/src/dsf/object_model/sbc/dsf/__init__.py
+++ b/src/dsf/object_model/sbc/dsf/__init__.py
@@ -2,5 +2,4 @@
from .http_endpoint import HttpEndpoint, HttpEndpointType
from .user_sessions import AccessLevel, SessionType, UserSession
-
-__all__ = ['DSF', 'HttpEndpoint', 'HttpEndpointType', 'AccessLevel', 'SessionType', 'UserSession']
+__all__ = ["DSF", "HttpEndpoint", "HttpEndpointType", "AccessLevel", "SessionType", "UserSession"]
diff --git a/src/dsf/object_model/sbc/dsf/communication_method.py b/src/dsf/object_model/sbc/dsf/communication_method.py
index a9d732a..d23fedd 100644
--- a/src/dsf/object_model/sbc/dsf/communication_method.py
+++ b/src/dsf/object_model/sbc/dsf/communication_method.py
@@ -8,4 +8,4 @@ class CommunicationMethod(str, Enum):
SPI = "spi"
# USB link adapter
- USB = "usb"
\ No newline at end of file
+ USB = "usb"
diff --git a/src/dsf/object_model/sbc/dsf/dsf.py b/src/dsf/object_model/sbc/dsf/dsf.py
index 6e47947..18fb918 100644
--- a/src/dsf/object_model/sbc/dsf/dsf.py
+++ b/src/dsf/object_model/sbc/dsf/dsf.py
@@ -9,17 +9,17 @@
class DSF(ModelObject):
"""Information about Duet Software Framework"""
- build_date_time = model_prop('build_date_time', str, "")
+ build_date_time = model_prop("build_date_time", str, "")
# Communication method used to talk to the firmware
- communication_method = model_prop('communication_method', CommunicationMethod, CommunicationMethod.SPI)
+ communication_method = model_prop("communication_method", CommunicationMethod, CommunicationMethod.SPI)
- http_endpoints = model_prop('http_endpoints', ModelCollection[HttpEndpoint], ModelCollection(HttpEndpoint))
- is64bit = model_prop('is64bit', bool, False)
- plugin_support = model_prop('plugin_support', bool, False)
- root_plugin_support = model_prop('root_plugin_support', bool, False)
- user_sessions = model_prop('user_sessions', ModelCollection[UserSession], ModelCollection(UserSession))
- version = model_prop('version', str, "")
+ http_endpoints = model_prop("http_endpoints", ModelCollection[HttpEndpoint], ModelCollection(HttpEndpoint))
+ is64bit = model_prop("is64bit", bool, False)
+ plugin_support = model_prop("plugin_support", bool, False)
+ root_plugin_support = model_prop("root_plugin_support", bool, False)
+ user_sessions = model_prop("user_sessions", ModelCollection[UserSession], ModelCollection(UserSession))
+ version = model_prop("version", str, "")
def __init__(self):
super().__init__()
diff --git a/src/dsf/object_model/sbc/dsf/http_endpoint.py b/src/dsf/object_model/sbc/dsf/http_endpoint.py
index 893469e..cfabc10 100644
--- a/src/dsf/object_model/sbc/dsf/http_endpoint.py
+++ b/src/dsf/object_model/sbc/dsf/http_endpoint.py
@@ -25,4 +25,3 @@ class HttpEndpoint(ModelObject):
def __init__(self):
super().__init__()
-
diff --git a/src/dsf/object_model/sbc/dsf/user_sessions/__init__.py b/src/dsf/object_model/sbc/dsf/user_sessions/__init__.py
index 5c92731..84a3a85 100644
--- a/src/dsf/object_model/sbc/dsf/user_sessions/__init__.py
+++ b/src/dsf/object_model/sbc/dsf/user_sessions/__init__.py
@@ -2,5 +2,4 @@
from .session_type import SessionType
from .user_sessions import UserSession
-__all__ = ['AccessLevel', 'SessionType', 'UserSession']
-
+__all__ = ["AccessLevel", "SessionType", "UserSession"]
diff --git a/src/dsf/object_model/sbc/sbc.py b/src/dsf/object_model/sbc/sbc.py
index 5fbc6b3..fafc9f4 100644
--- a/src/dsf/object_model/sbc/sbc.py
+++ b/src/dsf/object_model/sbc/sbc.py
@@ -11,16 +11,16 @@
class SBC(ModelObject):
"""Information about the SBC in SBC mode"""
- app_armor = model_prop('app_armor', bool, False)
- cpu = model_prop('cpu', CPU)
- dsf = model_prop('dsf', DSF)
- distribution = nullable_model_prop('distribution', str)
- distribution_build_time = nullable_model_prop('distribution_build_time', datetime, lambda: None)
- memory = model_prop('memory', Memory)
- model = nullable_model_prop('model', str)
- serial = nullable_model_prop('serial', str)
- upgrade = nullable_model_prop('upgrade', Upgrade)
- uptime = nullable_model_prop('uptime', float)
+ app_armor = model_prop("app_armor", bool, False)
+ cpu = model_prop("cpu", CPU)
+ dsf = model_prop("dsf", DSF)
+ distribution = nullable_model_prop("distribution", str)
+ distribution_build_time = nullable_model_prop("distribution_build_time", datetime, lambda: None)
+ memory = model_prop("memory", Memory)
+ model = nullable_model_prop("model", str)
+ serial = nullable_model_prop("serial", str)
+ upgrade = nullable_model_prop("upgrade", Upgrade)
+ uptime = nullable_model_prop("uptime", float)
def __init__(self):
- super().__init__()
\ No newline at end of file
+ super().__init__()
diff --git a/src/dsf/object_model/sbc/upgrade.py b/src/dsf/object_model/sbc/upgrade.py
index 6cacb3d..e922729 100644
--- a/src/dsf/object_model/sbc/upgrade.py
+++ b/src/dsf/object_model/sbc/upgrade.py
@@ -6,10 +6,10 @@ class Upgrade(ModelObject):
"""Details about a software upgrade in progress"""
# Description of the current upgrade step
- message = model_prop('message', str, "")
+ message = model_prop("message", str, "")
# Progress of the current upgrade step (0..1) or None if indeterminate
- progress = nullable_model_prop('progress', float)
+ progress = nullable_model_prop("progress", float)
def __init__(self):
super().__init__()
diff --git a/src/dsf/object_model/sensors/__init__.py b/src/dsf/object_model/sensors/__init__.py
index d551a39..12bcb9d 100644
--- a/src/dsf/object_model/sensors/__init__.py
+++ b/src/dsf/object_model/sensors/__init__.py
@@ -9,16 +9,15 @@
from .probe_type import ProbeType
from .sensors import Sensors
-
__all__ = [
- 'Accelerometer',
- 'AnalogSensor',
- 'AnalogSensorType',
- 'Endstop',
- 'EndstopType',
- 'GpInputPort',
- 'Probe',
- 'ProbeLoadCell',
- 'ProbeType',
- 'Sensors',
+ "Accelerometer",
+ "AnalogSensor",
+ "AnalogSensorType",
+ "Endstop",
+ "EndstopType",
+ "GpInputPort",
+ "Probe",
+ "ProbeLoadCell",
+ "ProbeType",
+ "Sensors",
]
diff --git a/src/dsf/object_model/sensors/analog_sensor.py b/src/dsf/object_model/sensors/analog_sensor.py
index 3266bfc..e66e13f 100644
--- a/src/dsf/object_model/sensors/analog_sensor.py
+++ b/src/dsf/object_model/sensors/analog_sensor.py
@@ -7,19 +7,19 @@
class AnalogSensor(ModelObject):
"""Representation of an analog sensor"""
- beta = nullable_model_prop('beta', float)
- c = nullable_model_prop('c', float)
- high_reading = nullable_model_prop('high_reading', float)
- last_reading = nullable_model_prop('last_reading', float)
- low_reading = nullable_model_prop('low_reading', float)
- name = nullable_model_prop('name', str)
- offset_adj = model_prop('offset_adj', float, 0.0)
- port = nullable_model_prop('port', str)
- r_25 = nullable_model_prop('r_25', float)
- r_ref = nullable_model_prop('r_ref', float)
- slope_adj = model_prop('slope_adj', float, 0.0)
- state = model_prop('state', TemperatureError, TemperatureError.ok)
- type = model_prop('type', AnalogSensorType, AnalogSensorType.Unknown)
+ beta = nullable_model_prop("beta", float)
+ c = nullable_model_prop("c", float)
+ high_reading = nullable_model_prop("high_reading", float)
+ last_reading = nullable_model_prop("last_reading", float)
+ low_reading = nullable_model_prop("low_reading", float)
+ name = nullable_model_prop("name", str)
+ offset_adj = model_prop("offset_adj", float, 0.0)
+ port = nullable_model_prop("port", str)
+ r_25 = nullable_model_prop("r_25", float)
+ r_ref = nullable_model_prop("r_ref", float)
+ slope_adj = model_prop("slope_adj", float, 0.0)
+ state = model_prop("state", TemperatureError, TemperatureError.ok)
+ type = model_prop("type", AnalogSensorType, AnalogSensorType.Unknown)
def __init__(self):
super(AnalogSensor, self).__init__()
diff --git a/src/dsf/object_model/sensors/analog_sensor_type.py b/src/dsf/object_model/sensors/analog_sensor_type.py
index e2a0eb9..46c31ef 100644
--- a/src/dsf/object_model/sensors/analog_sensor_type.py
+++ b/src/dsf/object_model/sensors/analog_sensor_type.py
@@ -57,10 +57,10 @@ class AnalogSensorType(str, Enum):
# ADS131 channel 0 (unipolar)
ADS131Chan0Unipolar = "ads131.chan0.u"
-
+
# ADS131 channel 0 (bipolar)
ADS131Chan0Bipolar = "ads131.chan0.b"
-
+
# ADS131 channel 1
ADS131Chan1 = "ads131.chan1"
diff --git a/src/dsf/object_model/sensors/filament_monitors/__init__.py b/src/dsf/object_model/sensors/filament_monitors/__init__.py
index 17d8979..301dfcd 100644
--- a/src/dsf/object_model/sensors/filament_monitors/__init__.py
+++ b/src/dsf/object_model/sensors/filament_monitors/__init__.py
@@ -3,31 +3,46 @@
from .filament_monitor_status import FilamentMonitorStatus
from .filament_monitor_type import FilamentMonitorType
from .laser_filament_monitor import LaserFilamentMonitor, LaserFilamentMonitorCalibrated, LaserFilamentMonitorConfigured
-from .pulsed_filament_monitor import PulsedFilamentMonitor, PulsedFilamentMonitorCalibrated, \
- PulsedFilamentMonitorConfigured
-from .rotating_magnet_filament_monitor import RotatingMagnetFilamentMonitor, RotatingMagnetFilamentMonitorCalibrated, \
- RotatingMagnetFilamentMonitorConfigured
+from .pulsed_filament_monitor import (
+ PulsedFilamentMonitor,
+ PulsedFilamentMonitorCalibrated,
+ PulsedFilamentMonitorConfigured,
+)
+from .rotating_magnet_filament_monitor import (
+ RotatingMagnetFilamentMonitor,
+ RotatingMagnetFilamentMonitorCalibrated,
+ RotatingMagnetFilamentMonitorConfigured,
+)
-
-__all_ = ['Duet3DFilamentMonitor', 'FilamentMonitor', 'FilamentMonitorStatus', 'FilamentMonitorType',
- 'LaserFilamentMonitor', 'LaserFilamentMonitorCalibrated', 'LaserFilamentMonitorConfigured',
- 'PulsedFilamentMonitor', 'PulsedFilamentMonitorCalibrated', 'PulsedFilamentMonitorConfigured',
- 'RotatingMagnetFilamentMonitor', 'RotatingMagnetFilamentMonitorCalibrated',
- 'RotatingMagnetFilamentMonitorConfigured']
+__all_ = [
+ "Duet3DFilamentMonitor",
+ "FilamentMonitor",
+ "FilamentMonitorStatus",
+ "FilamentMonitorType",
+ "LaserFilamentMonitor",
+ "LaserFilamentMonitorCalibrated",
+ "LaserFilamentMonitorConfigured",
+ "PulsedFilamentMonitor",
+ "PulsedFilamentMonitorCalibrated",
+ "PulsedFilamentMonitorConfigured",
+ "RotatingMagnetFilamentMonitor",
+ "RotatingMagnetFilamentMonitorCalibrated",
+ "RotatingMagnetFilamentMonitorConfigured",
+]
__all__ = [
- 'Duet3DFilamentMonitor',
- 'FilamentMonitor',
- 'FilamentMonitorStatus',
- 'FilamentMonitorType',
- 'LaserFilamentMonitor',
- 'LaserFilamentMonitorCalibrated',
- 'LaserFilamentMonitorConfigured',
- 'PulsedFilamentMonitor',
- 'PulsedFilamentMonitorCalibrated',
- 'PulsedFilamentMonitorConfigured',
- 'RotatingMagnetFilamentMonitor',
- 'RotatingMagnetFilamentMonitorCalibrated',
- 'RotatingMagnetFilamentMonitorConfigured',
+ "Duet3DFilamentMonitor",
+ "FilamentMonitor",
+ "FilamentMonitorStatus",
+ "FilamentMonitorType",
+ "LaserFilamentMonitor",
+ "LaserFilamentMonitorCalibrated",
+ "LaserFilamentMonitorConfigured",
+ "PulsedFilamentMonitor",
+ "PulsedFilamentMonitorCalibrated",
+ "PulsedFilamentMonitorConfigured",
+ "RotatingMagnetFilamentMonitor",
+ "RotatingMagnetFilamentMonitorCalibrated",
+ "RotatingMagnetFilamentMonitorConfigured",
]
diff --git a/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py b/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py
index b71f242..80f3b79 100644
--- a/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py
+++ b/src/dsf/object_model/sensors/filament_monitors/filament_monitor.py
@@ -48,7 +48,7 @@ def get_filament_monitor(type_: FilamentMonitorType | str) -> "FilamentMonitor":
def _update_from_json(self, **kwargs: JSONElement) -> "FilamentMonitor":
"""Override ObjectModel._update_from_json to return the FilamentMonitorType type matching the given type"""
- type_ = kwargs.get('type_')
+ type_ = kwargs.get("type_")
if isinstance(type_, str) and self.type != FilamentMonitorType(type_):
return self.get_filament_monitor(type_).update_from_json(kwargs)
diff --git a/src/dsf/object_model/sensors/filament_monitors/laser_filament_monitor.py b/src/dsf/object_model/sensors/filament_monitors/laser_filament_monitor.py
index c1fa5e8..0c9ea51 100644
--- a/src/dsf/object_model/sensors/filament_monitors/laser_filament_monitor.py
+++ b/src/dsf/object_model/sensors/filament_monitors/laser_filament_monitor.py
@@ -49,10 +49,10 @@ class LaserFilamentMonitor(Duet3DFilamentMonitor):
"""Information about a laser filament monitor"""
# Calibrated properties of this filament monitor
- calibrated = nullable_model_prop('calibrated', LaserFilamentMonitorCalibrated)
+ calibrated = nullable_model_prop("calibrated", LaserFilamentMonitorCalibrated)
# Configured properties of this filament monitor
- configured = model_prop('configured', LaserFilamentMonitorConfigured, LaserFilamentMonitorConfigured())
+ configured = model_prop("configured", LaserFilamentMonitorConfigured, LaserFilamentMonitorConfigured())
def __init__(self):
super(LaserFilamentMonitor, self).__init__(FilamentMonitorType.Laser)
diff --git a/src/dsf/object_model/sensors/filament_monitors/pulsed_filament_monitor.py b/src/dsf/object_model/sensors/filament_monitors/pulsed_filament_monitor.py
index cde7426..8c23342 100644
--- a/src/dsf/object_model/sensors/filament_monitors/pulsed_filament_monitor.py
+++ b/src/dsf/object_model/sensors/filament_monitors/pulsed_filament_monitor.py
@@ -46,13 +46,13 @@ class PulsedFilamentMonitor(FilamentMonitor):
"""Information about a pulsed filament monitor"""
# Calibrated properties of this filament monitor
- calibrated = nullable_model_prop('calibrated', PulsedFilamentMonitorCalibrated)
+ calibrated = nullable_model_prop("calibrated", PulsedFilamentMonitorCalibrated)
# Configured properties of this filament monitor
- configured = model_prop('configured', PulsedFilamentMonitorConfigured, PulsedFilamentMonitorConfigured())
+ configured = model_prop("configured", PulsedFilamentMonitorConfigured, PulsedFilamentMonitorConfigured())
# Position of the sensor (in mm)
- position = model_prop('position', float)
+ position = model_prop("position", float)
def __init__(self):
super(PulsedFilamentMonitor, self).__init__(FilamentMonitorType.Pulsed)
diff --git a/src/dsf/object_model/sensors/filament_monitors/rotating_magnet_filament_monitor.py b/src/dsf/object_model/sensors/filament_monitors/rotating_magnet_filament_monitor.py
index eb0cf1e..604437e 100644
--- a/src/dsf/object_model/sensors/filament_monitors/rotating_magnet_filament_monitor.py
+++ b/src/dsf/object_model/sensors/filament_monitors/rotating_magnet_filament_monitor.py
@@ -49,13 +49,13 @@ class RotatingMagnetFilamentMonitor(Duet3DFilamentMonitor):
"""Information about a rotating magnet filament monitor"""
# AGC reading of this filament monitor (None if unknown)
- agc = nullable_model_prop('agc', int)
+ agc = nullable_model_prop("agc", int)
# Calibrated properties of this filament monitor
- calibrated = nullable_model_prop('calibrated', RotatingMagnetFilamentMonitorCalibrated)
+ calibrated = nullable_model_prop("calibrated", RotatingMagnetFilamentMonitorCalibrated)
# Configured properties of this filament monitor
- configured = model_prop('configured', RotatingMagnetFilamentMonitorConfigured)
+ configured = model_prop("configured", RotatingMagnetFilamentMonitorConfigured)
def __init__(self):
super(RotatingMagnetFilamentMonitor, self).__init__(FilamentMonitorType.RotatingMagnet)
diff --git a/src/dsf/object_model/sensors/probe.py b/src/dsf/object_model/sensors/probe.py
index ad6b75b..bad3345 100644
--- a/src/dsf/object_model/sensors/probe.py
+++ b/src/dsf/object_model/sensors/probe.py
@@ -9,29 +9,31 @@
class Probe(ModelObject):
"""Information about a configured probe"""
- calibration_temperature = model_prop('calibration_temperature', float, 0)
- deployed_by_user = model_prop('deployed_by_user', bool, False)
- disables_heaters = model_prop('disables_heaters', bool, False)
+ calibration_temperature = model_prop("calibration_temperature", float, 0)
+ deployed_by_user = model_prop("deployed_by_user", bool, False)
+ disables_heaters = model_prop("disables_heaters", bool, False)
# Obsolete: use dive_heights instead
- dive_height = model_prop('dive_height', float, 0)
- dive_heights = model_prop('dive_heights', ModelCollection[float], ModelCollection(float, [0.0, 0.0]))
- is_calibrated = nullable_model_prop('is_calibrated', bool)
- last_stop_height = model_prop('last_stop_height', float, 0)
- load_cell = nullable_model_prop('load_cell', ProbeLoadCell)
- max_probe_count = model_prop('max_probe_count', int, 1)
- measured_height = nullable_model_prop('measured_height', float)
- offsets = model_prop('offsets', ModelCollection[float], ModelCollection(float, [0.0, 0.0]))
- recovery_time = model_prop('recovery_time', float, 0)
- scan_coefficients = nullable_model_prop('scan_coefficients', ModelCollection[float], lambda: ModelCollection(float))
- speeds = model_prop('speeds', ModelCollection[float], ModelCollection(float, [2.0, 2.0]))
- temperature_coefficients = model_prop('temperature_coefficients', ModelCollection[float], ModelCollection(float, [0.0, 0.0]))
- threshold = model_prop('threshold', int, 500)
- tolerance = model_prop('tolerance', float, 0.03)
- touch_mode = nullable_model_prop('touch_mode', ProbeTouchMode)
- travel_speed = model_prop('travel_speed', float, 6000)
- trigger_height = model_prop('trigger_height', float, 0.7)
- type = model_prop('type', ProbeType, ProbeType.NoProbe)
- value = model_prop('value', ModelCollection[int], ModelCollection(int))
+ dive_height = model_prop("dive_height", float, 0)
+ dive_heights = model_prop("dive_heights", ModelCollection[float], ModelCollection(float, [0.0, 0.0]))
+ is_calibrated = nullable_model_prop("is_calibrated", bool)
+ last_stop_height = model_prop("last_stop_height", float, 0)
+ load_cell = nullable_model_prop("load_cell", ProbeLoadCell)
+ max_probe_count = model_prop("max_probe_count", int, 1)
+ measured_height = nullable_model_prop("measured_height", float)
+ offsets = model_prop("offsets", ModelCollection[float], ModelCollection(float, [0.0, 0.0]))
+ recovery_time = model_prop("recovery_time", float, 0)
+ scan_coefficients = nullable_model_prop("scan_coefficients", ModelCollection[float], lambda: ModelCollection(float))
+ speeds = model_prop("speeds", ModelCollection[float], ModelCollection(float, [2.0, 2.0]))
+ temperature_coefficients = model_prop(
+ "temperature_coefficients", ModelCollection[float], ModelCollection(float, [0.0, 0.0])
+ )
+ threshold = model_prop("threshold", int, 500)
+ tolerance = model_prop("tolerance", float, 0.03)
+ touch_mode = nullable_model_prop("touch_mode", ProbeTouchMode)
+ travel_speed = model_prop("travel_speed", float, 6000)
+ trigger_height = model_prop("trigger_height", float, 0.7)
+ type = model_prop("type", ProbeType, ProbeType.NoProbe)
+ value = model_prop("value", ModelCollection[int], ModelCollection(int))
def __init__(self):
super(Probe, self).__init__()
diff --git a/src/dsf/object_model/sensors/probe_touch_mode.py b/src/dsf/object_model/sensors/probe_touch_mode.py
index f44e1bf..277421c 100644
--- a/src/dsf/object_model/sensors/probe_touch_mode.py
+++ b/src/dsf/object_model/sensors/probe_touch_mode.py
@@ -11,4 +11,4 @@ class ProbeTouchMode(ModelObject):
trigger_height = model_prop("trigger_height", float)
def __init__(self):
- super(ProbeTouchMode, self).__init__()
\ No newline at end of file
+ super(ProbeTouchMode, self).__init__()
diff --git a/src/dsf/object_model/sensors/sensors.py b/src/dsf/object_model/sensors/sensors.py
index b6d5e23..0205989 100644
--- a/src/dsf/object_model/sensors/sensors.py
+++ b/src/dsf/object_model/sensors/sensors.py
@@ -15,7 +15,9 @@ class Sensors(ModelObject):
"""Information about sensors"""
# List of configured accelerometers, indexed by their M955/M956 P number
- accelerometers = model_prop("accelerometers", ModelCollection[Optional[Accelerometer]], ModelCollection(Optional[Accelerometer]))
+ accelerometers = model_prop(
+ "accelerometers", ModelCollection[Optional[Accelerometer]], ModelCollection(Optional[Accelerometer])
+ )
# List of analog sensors
analog = model_prop("analog", ModelCollection[Optional[AnalogSensor]], ModelCollection(Optional[AnalogSensor]))
@@ -24,7 +26,9 @@ class Sensors(ModelObject):
endstops = model_prop("endstops", ModelCollection[Optional[Endstop]], ModelCollection(Optional[Endstop]))
# List of configured filament monitors
- filament_monitors = model_prop("filament_monitors", ModelCollection[Optional[FilamentMonitor]], ModelCollection(Optional[FilamentMonitor]))
+ filament_monitors = model_prop(
+ "filament_monitors", ModelCollection[Optional[FilamentMonitor]], ModelCollection(Optional[FilamentMonitor])
+ )
# List of general-purpose input ports
gp_in = model_prop("gp_in", ModelCollection[Optional[GpInputPort]], ModelCollection(Optional[GpInputPort]))
diff --git a/src/dsf/object_model/spindles/__init__.py b/src/dsf/object_model/spindles/__init__.py
index c7b00a6..5f6b68e 100644
--- a/src/dsf/object_model/spindles/__init__.py
+++ b/src/dsf/object_model/spindles/__init__.py
@@ -1,4 +1,4 @@
from .spindles import Spindle
from .spindle_state import SpindleState
-__all__ = ['Spindle', 'SpindleState']
+__all__ = ["Spindle", "SpindleState"]
diff --git a/src/dsf/object_model/state/__init__.py b/src/dsf/object_model/state/__init__.py
index f25d396..8542d5e 100644
--- a/src/dsf/object_model/state/__init__.py
+++ b/src/dsf/object_model/state/__init__.py
@@ -7,15 +7,14 @@
from .startup_error import StartupError
from .state import State
-
__all__ = [
- 'BeepRequest',
- 'GpOutputPort',
- 'LogLevel',
- 'MachineStatus',
- 'MessageBox',
- 'MessageBoxMode',
- 'RestorePoint',
- 'StartupError',
- 'State',
+ "BeepRequest",
+ "GpOutputPort",
+ "LogLevel",
+ "MachineStatus",
+ "MessageBox",
+ "MessageBoxMode",
+ "RestorePoint",
+ "StartupError",
+ "State",
]
diff --git a/src/dsf/object_model/state/message_box.py b/src/dsf/object_model/state/message_box.py
index 4d8c2d0..740bd80 100644
--- a/src/dsf/object_model/state/message_box.py
+++ b/src/dsf/object_model/state/message_box.py
@@ -35,17 +35,17 @@ class MessageBoxMode(IntEnum):
class MessageBox(ModelObject):
"""Information about the message box to show"""
- axis_controls = nullable_model_prop('axis_controls', int)
- cancel_button = model_prop('cancel_button', bool, False)
- choices = nullable_model_prop('choices', ModelCollection[str], lambda: ModelCollection(str))
- default = nullable_model_prop('default', object)
- max = nullable_model_prop('max', float)
- message = model_prop('message', str, "")
- min = nullable_model_prop('min', float)
- mode = model_prop('mode', MessageBoxMode, MessageBoxMode.OkOnly)
- seq = model_prop('seq', int, -1)
- timeout = model_prop('timeout', int, 0)
- title = model_prop('title', str, "")
+ axis_controls = nullable_model_prop("axis_controls", int)
+ cancel_button = model_prop("cancel_button", bool, False)
+ choices = nullable_model_prop("choices", ModelCollection[str], lambda: ModelCollection(str))
+ default = nullable_model_prop("default", object)
+ max = nullable_model_prop("max", float)
+ message = model_prop("message", str, "")
+ min = nullable_model_prop("min", float)
+ mode = model_prop("mode", MessageBoxMode, MessageBoxMode.OkOnly)
+ seq = model_prop("seq", int, -1)
+ timeout = model_prop("timeout", int, 0)
+ title = model_prop("title", str, "")
def __init__(self):
super(MessageBox, self).__init__()
diff --git a/src/dsf/object_model/state/restore_point.py b/src/dsf/object_model/state/restore_point.py
index 2cc60a7..7f4004d 100644
--- a/src/dsf/object_model/state/restore_point.py
+++ b/src/dsf/object_model/state/restore_point.py
@@ -8,14 +8,14 @@ class RestorePoint(ModelObject):
Class holding information about a restore point
"""
- coords = model_prop('coords', ModelCollection[float], ModelCollection(float))
- extruder_pos = model_prop('extruder_pos', float, 0)
- fan_pwm = model_prop('fan_pwm', float, 0)
- feed_rate = model_prop('feed_rate', float, 0)
- g_command_number = model_prop('g_command_number', int, -1)
- io_bits = nullable_model_prop('io_bits', int)
- laser_pwm = nullable_model_prop('laser_pwm', float)
- tool_number = model_prop('tool_number', int, -1)
+ coords = model_prop("coords", ModelCollection[float], ModelCollection(float))
+ extruder_pos = model_prop("extruder_pos", float, 0)
+ fan_pwm = model_prop("fan_pwm", float, 0)
+ feed_rate = model_prop("feed_rate", float, 0)
+ g_command_number = model_prop("g_command_number", int, -1)
+ io_bits = nullable_model_prop("io_bits", int)
+ laser_pwm = nullable_model_prop("laser_pwm", float)
+ tool_number = model_prop("tool_number", int, -1)
def __init__(self):
super(RestorePoint, self).__init__()
diff --git a/src/dsf/object_model/state/state.py b/src/dsf/object_model/state/state.py
index d5e74a7..9dd1b3e 100644
--- a/src/dsf/object_model/state/state.py
+++ b/src/dsf/object_model/state/state.py
@@ -17,31 +17,31 @@
class State(ModelObject):
"""Information about the machine state"""
- atx_power = nullable_model_prop('atx_power', bool)
- atx_power_port = nullable_model_prop('atx_power_port', str)
- beep = nullable_model_prop('beep', BeepRequest)
- current_tool = model_prop('current_tool', int, -1)
- deferred_power_down = nullable_model_prop('deferred_power_down', bool)
- display_message = model_prop('display_message', str, "")
- gp_out = model_prop('gp_out', ModelCollection[Optional[GpOutputPort]], ModelCollection(Optional[GpOutputPort]))
- laser_pwm = nullable_model_prop('laser_pwm', float)
- log_file = nullable_model_prop('log_file', str)
- log_level = model_prop('log_level', LogLevel, LogLevel.Off)
- message_box = nullable_model_prop('message_box', MessageBox)
- machine_mode = model_prop('machine_mode', MachineMode, MachineMode.FFF)
- macro_restarted = model_prop('macro_restarted', bool, False)
- ms_up_time = model_prop('ms_up_time', int)
- next_tool = model_prop('next_tool', int, -1)
- plugins_started = model_prop('plugins_started', bool, False)
- power_fail_script = model_prop('power_fail_script', str, "")
- previous_tool = model_prop('previous_tool', int, -1)
- restore_points = model_prop('restore_points', ModelCollection[RestorePoint], ModelCollection(RestorePoint))
- startup_error = nullable_model_prop('startup_error', StartupError)
- status = model_prop('status', MachineStatus, MachineStatus.starting)
- this_active = nullable_model_prop('this_active', bool)
- this_input = nullable_model_prop('this_input', int)
- time = nullable_model_prop('time', datetime, lambda: None)
- up_time = model_prop('up_time', int, 0)
+ atx_power = nullable_model_prop("atx_power", bool)
+ atx_power_port = nullable_model_prop("atx_power_port", str)
+ beep = nullable_model_prop("beep", BeepRequest)
+ current_tool = model_prop("current_tool", int, -1)
+ deferred_power_down = nullable_model_prop("deferred_power_down", bool)
+ display_message = model_prop("display_message", str, "")
+ gp_out = model_prop("gp_out", ModelCollection[Optional[GpOutputPort]], ModelCollection(Optional[GpOutputPort]))
+ laser_pwm = nullable_model_prop("laser_pwm", float)
+ log_file = nullable_model_prop("log_file", str)
+ log_level = model_prop("log_level", LogLevel, LogLevel.Off)
+ message_box = nullable_model_prop("message_box", MessageBox)
+ machine_mode = model_prop("machine_mode", MachineMode, MachineMode.FFF)
+ macro_restarted = model_prop("macro_restarted", bool, False)
+ ms_up_time = model_prop("ms_up_time", int)
+ next_tool = model_prop("next_tool", int, -1)
+ plugins_started = model_prop("plugins_started", bool, False)
+ power_fail_script = model_prop("power_fail_script", str, "")
+ previous_tool = model_prop("previous_tool", int, -1)
+ restore_points = model_prop("restore_points", ModelCollection[RestorePoint], ModelCollection(RestorePoint))
+ startup_error = nullable_model_prop("startup_error", StartupError)
+ status = model_prop("status", MachineStatus, MachineStatus.starting)
+ this_active = nullable_model_prop("this_active", bool)
+ this_input = nullable_model_prop("this_input", int)
+ time = nullable_model_prop("time", datetime, lambda: None)
+ up_time = model_prop("up_time", int, 0)
def __init__(self):
super(State, self).__init__()
diff --git a/src/dsf/object_model/tools/__init__.py b/src/dsf/object_model/tools/__init__.py
index 9ea29dc..44b9742 100644
--- a/src/dsf/object_model/tools/__init__.py
+++ b/src/dsf/object_model/tools/__init__.py
@@ -2,4 +2,4 @@
from .tool_state import ToolState
from .tool_retraction import ToolRetraction
-__all__ = ['Tool', 'ToolState', 'ToolRetraction']
+__all__ = ["Tool", "ToolState", "ToolRetraction"]
diff --git a/src/dsf/object_model/tools/tool_retraction.py b/src/dsf/object_model/tools/tool_retraction.py
index f12eaee..7bcab9e 100644
--- a/src/dsf/object_model/tools/tool_retraction.py
+++ b/src/dsf/object_model/tools/tool_retraction.py
@@ -21,4 +21,4 @@ class ToolRetraction(ModelObject):
z_hop = model_prop("z_hop", float, 0)
def __init__(self):
- super().__init__()
+ super().__init__()
diff --git a/src/dsf/object_model/tools/tools.py b/src/dsf/object_model/tools/tools.py
index 510e4be..620d4ff 100644
--- a/src/dsf/object_model/tools/tools.py
+++ b/src/dsf/object_model/tools/tools.py
@@ -8,28 +8,28 @@
class Tool(ModelObject):
"""Information about a configured tool"""
- active = model_prop('active', ModelCollection[float], ModelCollection(float))
- axes = model_prop('axes', ModelCollection[list[int]], ModelCollection(list[int]))
- extruders = model_prop('extruders', ModelCollection[int], ModelCollection(int))
- fans = model_prop('fans', ModelCollection[int], ModelCollection(int))
+ active = model_prop("active", ModelCollection[float], ModelCollection(float))
+ axes = model_prop("axes", ModelCollection[list[int]], ModelCollection(list[int]))
+ extruders = model_prop("extruders", ModelCollection[int], ModelCollection(int))
+ fans = model_prop("fans", ModelCollection[int], ModelCollection(int))
# Obsolete: use feed_forward_pwm instead
- feed_forward = model_prop('feed_forward', ModelCollection[float], ModelCollection(float))
- feed_forward_advance = nullable_model_prop('feed_forward_advance', float)
- feed_forward_pwm = model_prop('feed_forward_pwm', ModelCollection[float], ModelCollection(float))
- feed_forward_temp = model_prop('feed_forward_temp', ModelCollection[float], ModelCollection(float))
- filament_extruder = model_prop('filament_extruder', int, -1)
- heaters = model_prop('heaters', ModelCollection[int], ModelCollection(int))
- is_retracted = model_prop('is_retracted', bool, False)
- mix = model_prop('mix', ModelCollection[float], ModelCollection(float))
- name = model_prop('name', str, "")
- number = model_prop('number', int, 0)
- offsets = model_prop('offsets', ModelCollection[float], ModelCollection(float))
- offsets_probed = model_prop('offsets_probed', int, 0)
- retraction = model_prop('retraction', ToolRetraction)
- spindle = model_prop('spindle', int, -1)
- spindle_rpm = model_prop('spindle_rpm', int, 0)
- standby = model_prop('standby', ModelCollection[float], ModelCollection(float))
- state = model_prop('state', ToolState, ToolState.off)
+ feed_forward = model_prop("feed_forward", ModelCollection[float], ModelCollection(float))
+ feed_forward_advance = nullable_model_prop("feed_forward_advance", float)
+ feed_forward_pwm = model_prop("feed_forward_pwm", ModelCollection[float], ModelCollection(float))
+ feed_forward_temp = model_prop("feed_forward_temp", ModelCollection[float], ModelCollection(float))
+ filament_extruder = model_prop("filament_extruder", int, -1)
+ heaters = model_prop("heaters", ModelCollection[int], ModelCollection(int))
+ is_retracted = model_prop("is_retracted", bool, False)
+ mix = model_prop("mix", ModelCollection[float], ModelCollection(float))
+ name = model_prop("name", str, "")
+ number = model_prop("number", int, 0)
+ offsets = model_prop("offsets", ModelCollection[float], ModelCollection(float))
+ offsets_probed = model_prop("offsets_probed", int, 0)
+ retraction = model_prop("retraction", ToolRetraction)
+ spindle = model_prop("spindle", int, -1)
+ spindle_rpm = model_prop("spindle_rpm", int, 0)
+ standby = model_prop("standby", ModelCollection[float], ModelCollection(float))
+ state = model_prop("state", ToolState, ToolState.off)
def __init__(self):
super().__init__()
diff --git a/src/dsf/object_model/utils.py b/src/dsf/object_model/utils.py
index a7c46cf..cf01fae 100644
--- a/src/dsf/object_model/utils.py
+++ b/src/dsf/object_model/utils.py
@@ -8,18 +8,15 @@
from .model_type import ModelType
from ..utils import JSONObj, JSONElement
-
_TProperty = TypeVar("_TProperty", covariant=True)
class TypedReadableProperty(Protocol[_TProperty]):
@overload
- def __get__(self, obj: None, objtype: Optional[type[object]] = None) -> "TypedReadableProperty[_TProperty]":
- ...
+ def __get__(self, obj: None, objtype: Optional[type[object]] = None) -> "TypedReadableProperty[_TProperty]": ...
@overload
- def __get__(self, obj: object, objtype: Optional[type[object]] = None) -> _TProperty:
- ...
+ def __get__(self, obj: object, objtype: Optional[type[object]] = None) -> _TProperty: ...
T = TypeVar("T")
@@ -40,26 +37,35 @@ def _set_model_prop(
current_value.update_from_json(None)
elif isinstance(value, dict): # Update from JSON
if not isinstance(current_value, (ModelObject, ModelDictionary)):
- raise TypeError(f"{instance.__class__.__name__}.{name} must be of type ModelObject or ModelDictionary to update from a dict."
- f" Got {type(current_value).__name__}: {current_value}")
+ raise TypeError(
+ f"{instance.__class__.__name__}.{name} must be of type ModelObject or ModelDictionary to update from a dict."
+ f" Got {type(current_value).__name__}: {current_value}"
+ )
current_value.update_from_json(cast(JSONObj, value))
elif isinstance(value, list):
if not isinstance(current_value, ModelCollection):
- raise TypeError(f"{instance.__class__.__name__}.{name} must be of type ModelCollection to update from a list."
- f" Got {type(current_value).__name__}: {current_value}")
+ raise TypeError(
+ f"{instance.__class__.__name__}.{name} must be of type ModelCollection to update from a list."
+ f" Got {type(current_value).__name__}: {current_value}"
+ )
current_value.update_from_json(cast(list[JSONElement], value))
elif runtime_type is datetime and isinstance(value, str):
try:
parsed_date = datetime.fromisoformat(value)
setattr(instance, name, parsed_date)
except ValueError:
- raise TypeError(f"{instance.__class__.__name__}.{name} must be a valid ISO format datetime string to update from JSON. Got: {value}")
+ raise TypeError(
+ f"{instance.__class__.__name__}.{name} must be a valid ISO format datetime string to update from JSON. Got: {value}"
+ )
elif isinstance(value, (str, int, float, bool)):
converter = cast(Callable[[object], object], runtime_type)
setattr(instance, name, converter(value))
else:
- raise TypeError(f"{instance.__class__.__name__}.{name} must be of type {runtime_type} or a compatible JSON element to update from."
- f" Got {type(value).__name__}: {value}")
+ raise TypeError(
+ f"{instance.__class__.__name__}.{name} must be of type {runtime_type} or a compatible JSON element to update from."
+ f" Got {type(value).__name__}: {value}"
+ )
+
def model_prop(name: str, model_type: type[T], default: Optional[T] = None) -> TypedReadableProperty[T]:
"""
@@ -70,7 +76,7 @@ def model_prop(name: str, model_type: type[T], default: Optional[T] = None) -> T
:return:
"""
- STORAGE_NAME = '_' + name
+ STORAGE_NAME = "_" + name
runtime_model_type = cast(type[object], get_origin(model_type) or model_type)
# Mutable model defaults must never be shared between instances.
@@ -105,7 +111,7 @@ def get_or_create_value() -> T:
if isinstance(value, runtime_model_type):
setattr(self, STORAGE_NAME, value)
return
-
+
current_value = get_or_create_value()
_set_model_prop(self, STORAGE_NAME, runtime_model_type, current_value, value)
@@ -123,7 +129,7 @@ def nullable_model_prop(
:return: TypedReadableProperty[Optional[T]]
"""
- STORAGE_NAME = '_' + name
+ STORAGE_NAME = "_" + name
runtime_model_type = cast(type[object], get_origin(model_type) or model_type)
model_type_name = getattr(runtime_model_type, "__name__", str(model_type))
@@ -131,7 +137,9 @@ def nullable_model_prop(
try:
model_type() # validate that the default constructor works
except Exception:
- raise TypeError(f"Default constructor failed for type {model_type_name}. Provide a constructor function to create default values for the property {name}.")
+ raise TypeError(
+ f"Default constructor failed for type {model_type_name}. Provide a constructor function to create default values for the property {name}."
+ )
_factory: Callable[[], Optional[T]] = model_type
else:
_factory = constructor
diff --git a/src/dsf/object_model/volumes/__init__.py b/src/dsf/object_model/volumes/__init__.py
index 42acc50..a7627d7 100644
--- a/src/dsf/object_model/volumes/__init__.py
+++ b/src/dsf/object_model/volumes/__init__.py
@@ -1,3 +1,3 @@
from .volumes import Volume
-__all__ = ['Volume']
+__all__ = ["Volume"]
diff --git a/src/dsf/object_model/volumes/volumes.py b/src/dsf/object_model/volumes/volumes.py
index 0cb60e2..f9eee1a 100644
--- a/src/dsf/object_model/volumes/volumes.py
+++ b/src/dsf/object_model/volumes/volumes.py
@@ -6,28 +6,28 @@ class Volume(ModelObject):
"""Information about a storage device"""
# Total capacity of the storage device (in bytes or null)
- capacity = nullable_model_prop('capacity', int)
-
+ capacity = nullable_model_prop("capacity", int)
+
# How much space is still available on this device (in bytes or null)
- free_space = nullable_model_prop('free_space', int)
-
+ free_space = nullable_model_prop("free_space", int)
+
# Whether the storage device is mounted
- mounted = model_prop('mounted', bool)
-
+ mounted = model_prop("mounted", bool)
+
# Name of this volume
- name = nullable_model_prop('name', str)
-
+ name = nullable_model_prop("name", str)
+
# Whether any file is open on this volume or null if unknown
- open_files = nullable_model_prop('open_files', bool)
-
+ open_files = nullable_model_prop("open_files", bool)
+
# Total size of this volume (in bytes or null)
- partition_size = nullable_model_prop('partition_size', int)
-
+ partition_size = nullable_model_prop("partition_size", int)
+
# Logical path of the storage device
- path = nullable_model_prop('path', str)
-
+ path = nullable_model_prop("path", str)
+
# Speed of the storage device (in bytes/s or null if unknown)
- speed = nullable_model_prop('speed', int)
+ speed = nullable_model_prop("speed", int)
def __init__(self):
super().__init__()
diff --git a/src/dsf/utils.py b/src/dsf/utils.py
index 5f48b4d..5e59ea5 100644
--- a/src/dsf/utils.py
+++ b/src/dsf/utils.py
@@ -5,6 +5,7 @@
from types import UnionType
from typing import Any, Optional, Callable, TypeVar, TypeAlias, Union, get_args, get_origin, cast, overload
+
# We don't want our deprecations to be ignored by default, so create our own type.
class DeprecatedWarning(UserWarning):
pass
@@ -86,29 +87,36 @@ def camel_to_snake(s: str, keep_acronyms: bool = True) -> str:
:param keep_acronyms: Wheter acronyms should be kept uppercase or not
:returns: The string in snake_case format"""
# Added a look-behind (?!^) so initials like SBC are not getting snake-cased
- snake = re.sub(r'((?<=[a-z])[A-Z0-9]|(?!^)[A-Z0-9](?=[a-z]))', r'_\1', s)
- return '_'.join(w if w.isupper() else w.lower() for w in snake.split('_')) if keep_acronyms else snake.lower()
+ snake = re.sub(r"((?<=[a-z])[A-Z0-9]|(?!^)[A-Z0-9](?=[a-z]))", r"_\1", s)
+ return "_".join(w if w.isupper() else w.lower() for w in snake.split("_")) if keep_acronyms else snake.lower()
-F = TypeVar('F', bound=Callable[..., Any])
+F = TypeVar("F", bound=Callable[..., Any])
def deprecated(instructions: str) -> Callable[[F], F]:
"""Flags a function/method as deprecated.
:param instructions: A human-friendly string of instructions
"""
+
def decorator(func: F) -> F:
"""This is a decorator which can be used to mark functions as deprecated.
It will result in a warning being emitted when the function is used."""
+
def deprecated_func(*args: Any, **kwargs: Any) -> Any:
# Do not show DeprecatedWarning on ObjectModel update (function called by update_from_json)
frame = inspect.currentframe()
if frame is not None and frame.f_back is not None:
- if frame.f_back.f_code.co_name not in ['_update_from_json', 'update_from_json']:
- warnings.warn(f"Call to deprecated function {func.__name__}(). {instructions}",
- DeprecatedWarning, stacklevel=2)
+ if frame.f_back.f_code.co_name not in ["_update_from_json", "update_from_json"]:
+ warnings.warn(
+ f"Call to deprecated function {func.__name__}(). {instructions}",
+ DeprecatedWarning,
+ stacklevel=2,
+ )
return func(*args, **kwargs)
+
return deprecated_func # type: ignore[return-value]
+
return decorator # type: ignore[return-value]
@@ -118,7 +126,7 @@ def preserve_builtin(data: Optional[JSONObj]) -> JSONObj:
to avoid name shadowing. e.g: type => type_"""
if data is None:
return {}
- reserved_keys = ['format', 'global', 'id', 'license', 'max', 'min', 'None', 'type']
+ reserved_keys = ["format", "global", "id", "license", "max", "min", "None", "type"]
return {f"{k}_" if k in reserved_keys else k: v for k, v in data.items()}
@@ -128,5 +136,5 @@ def snake_to_camel(s: str, first_lower: bool = True, keep_acronyms: bool = True)
:param first_lower: Wheter the first character is returned as lower case or not
:param keep_acronyms: Wheter acronyms should be kept uppercase or not
:returns: The string in CamelCase format"""
- res = ''.join(w if w.isupper() and keep_acronyms else w.title() for w in s.split('_'))
- return f'{res[0].lower()}{res[1:]}' if first_lower and len(res) else res
+ res = "".join(w if w.isupper() and keep_acronyms else w.title() for w in s.split("_"))
+ return f"{res[0].lower()}{res[1:]}" if first_lower and len(res) else res
diff --git a/tests/test_base_connection.py b/tests/test_base_connection.py
index 887590d..60d4b09 100644
--- a/tests/test_base_connection.py
+++ b/tests/test_base_connection.py
@@ -18,7 +18,7 @@ def test_has_data_available_returns_false_for_partial_buffer_without_socket_data
connection.input = '{"key"'
connection.socket = Mock(spec=socket.socket)
- with patch('src.dsf.connections.base_connection.select.select', return_value=([], [], [])):
+ with patch("src.dsf.connections.base_connection.select.select", return_value=([], [], [])):
self.assertFalse(connection.has_data_available())
def test_has_data_available_returns_true_when_socket_is_readable(self):
@@ -26,7 +26,7 @@ def test_has_data_available_returns_true_when_socket_is_readable(self):
connection.socket = Mock(spec=socket.socket)
with patch(
- 'src.dsf.connections.base_connection.select.select',
+ "src.dsf.connections.base_connection.select.select",
return_value=([connection.socket], [], []),
):
self.assertTrue(connection.has_data_available())
@@ -54,5 +54,5 @@ def test_receive_json_raises_when_server_closes_connection(self):
self.assertLess(time.monotonic() - start, 5)
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_code.py b/tests/test_code.py
index 981366a..3922db3 100644
--- a/tests/test_code.py
+++ b/tests/test_code.py
@@ -28,7 +28,7 @@ def test_code(self):
self.assertEqual(c.filePosition, None)
self.assertEqual(c.indent, 0)
self.assertEqual(c.channel, CodeChannel.HTTP)
- self.assertEqual(c.command, 'Code')
+ self.assertEqual(c.command, "Code")
def test_code_keyword(self):
json_str = '{"sourceConnection":33,"result":null,"type":"K","channel":"HTTP","lineNumber":null,"indent":0,"keyword":9,"keywordArgument":"test","majorNumber":null,"minorNumber":null,"flags":2048,"comment":null,"filePosition":null,"length":12,"parameters":[],"command":"Code"}'
diff --git a/tests/test_commands.py b/tests/test_commands.py
index fc464f4..cedd5d5 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -12,27 +12,39 @@ def serialize(obj: object) -> dict[str, Any]:
class TestCommands(unittest.TestCase):
def test_get_object_model(self):
- self.assertEqual(serialize(object_model.get_object_model(["move.axes"])),
- {"command": "GetObjectModel", "filters": ["move.axes"]})
+ self.assertEqual(
+ serialize(object_model.get_object_model(["move.axes"])),
+ {"command": "GetObjectModel", "filters": ["move.axes"]},
+ )
def test_query_object_model(self):
- self.assertEqual(serialize(object_model.query_object_model("heat", "fn")),
- {"command": "QueryObjectModel", "key": "heat", "flags": "fn"})
+ self.assertEqual(
+ serialize(object_model.query_object_model("heat", "fn")),
+ {"command": "QueryObjectModel", "key": "heat", "flags": "fn"},
+ )
def test_set_network_protocol(self):
- self.assertEqual(serialize(object_model.set_network_protocol("http", True)),
- {"command": "SetNetworkProtocol", "protocol": "http", "enabled": True})
+ self.assertEqual(
+ serialize(object_model.set_network_protocol("http", True)),
+ {"command": "SetNetworkProtocol", "protocol": "http", "enabled": True},
+ )
def test_set_wifi_country(self):
- self.assertEqual(serialize(object_model.set_wifi_country("GB")),
- {"command": "SetWifiCountry", "countryCode": "GB"})
+ self.assertEqual(
+ serialize(object_model.set_wifi_country("GB")), {"command": "SetWifiCountry", "countryCode": "GB"}
+ )
def test_set_update_status(self):
- self.assertEqual(serialize(generic.set_update_status(True, "Installing", 0.25)),
- {"command": "SetUpdateStatus", "updating": True, "message": "Installing", "progress": 0.25})
+ self.assertEqual(
+ serialize(generic.set_update_status(True, "Installing", 0.25)),
+ {"command": "SetUpdateStatus", "updating": True, "message": "Installing", "progress": 0.25},
+ )
def test_subscribe_init_message(self):
- message = serialize(client_init_messages.subscribe_init_message(
- client_init_messages.SubscriptionMode.PATCH, ["state"], verbose=True, obsolete=True))
+ message = serialize(
+ client_init_messages.subscribe_init_message(
+ client_init_messages.SubscriptionMode.PATCH, ["state"], verbose=True, obsolete=True
+ )
+ )
self.assertTrue(message["verbose"])
self.assertTrue(message["obsolete"])
diff --git a/tests/test_custom_http_endpoint.py b/tests/test_custom_http_endpoint.py
index 01f305f..4d9972e 100644
--- a/tests/test_custom_http_endpoint.py
+++ b/tests/test_custom_http_endpoint.py
@@ -14,7 +14,7 @@
async def respond_something(http_endpoint_connection: HttpEndpointConnection):
r = await http_endpoint_connection.read_request()
- if (len(r.body) > 0):
+ if len(r.body) > 0:
data = json.loads(r.body)
print(data)
await http_endpoint_connection.send_response(200, "so happy you asked for it!", HttpResponseType.PlainText)
@@ -60,13 +60,14 @@ def mock_dcs(self):
http_endpoint_msg = conn.recv(1024)
self.assertEqual(
- json.loads(http_endpoint_msg.decode()), {
+ json.loads(http_endpoint_msg.decode()),
+ {
"command": "AddHttpEndpoint",
"endpointType": "GET",
"namespace": "custom",
"path": "getIt",
- "isUploadRequest": False
- }
+ "isUploadRequest": False,
+ },
)
conn.sendall('{"result":"/var/run/dsf/custom/getIt-GET.sock","success":true}'.encode())
diff --git a/tests/test_custom_m_codes.py b/tests/test_custom_m_codes.py
index bd6e505..6a96b40 100644
--- a/tests/test_custom_m_codes.py
+++ b/tests/test_custom_m_codes.py
@@ -46,52 +46,55 @@ def mock_dcs(self):
setup_msg = conn.recv(1024)
self.assertIn(b'"mode":"Intercept"', setup_msg)
self.assertIn(b'"interceptionMode":"Pre"', setup_msg)
-
+
conn.sendall(b'{"success":true}')
-
+
# Send M1234 code
conn.sendall(
b"{"
- b'"connection":{"id":12,"apiVersion":' + str(PROTOCOL_VERSION).encode() + b',"isConnected":true},"sourceConnection":12,'
+ b'"connection":{"id":12,"apiVersion":'
+ + str(PROTOCOL_VERSION).encode()
+ + b',"isConnected":true},"sourceConnection":12,'
b'"result":null,"type":"M","channel":"HTTP","lineNumber":null,"indent":0,"keyword":0,'
b'"keywordArgument":null,"majorNumber":1234,"minorNumber":null,"flags":2048,"comment":null,'
b'"filePosition":null,"length":6,"parameters":[],"command":"Code"'
b"}"
)
-
+
# Process responses more flexibly
response1 = json.loads(conn.recv(1024))
# Either a Flush or Resolve command is acceptable based on implementation
self.assertIn("command", response1)
-
+
# Send appropriate response based on what was received
if response1.get("command") == "Flush":
conn.sendall(b'{"result":true,"success":true}')
# After Flush, there should be a Resolve
response2 = json.loads(conn.recv(1024))
self.assertEqual(response2.get("command"), "Resolve")
-
+
# Send M5678 code
conn.sendall(
b"{"
- b'"connection":{"id":12,"apiVersion":' + str(PROTOCOL_VERSION).encode() + b',"isConnected":true},"sourceConnection":12,'
+ b'"connection":{"id":12,"apiVersion":'
+ + str(PROTOCOL_VERSION).encode()
+ + b',"isConnected":true},"sourceConnection":12,'
b'"result":null,"type":"M","channel":"HTTP","lineNumber":null,"indent":0,"keyword":0,'
b'"keywordArgument":null,"majorNumber":5678,"minorNumber":null,"flags":2048,"comment":null,'
b'"filePosition":null,"length":6,"parameters":[],"command":"Code"'
b"}"
)
-
+
# Expect a response for M5678
response3 = json.loads(conn.recv(1024))
self.assertIn("command", response3)
-
+
conn.close()
self.dcs_passed.set() # indicate that all asserts passed and the mock_dcs is shutting down
def test_custom_m_codes(self):
filters = ["M1234", "M5678"]
- intercept_connection = InterceptConnection(
- InterceptionMode.PRE, filters=filters, debug=True, timeout=3)
+ intercept_connection = InterceptConnection(InterceptionMode.PRE, filters=filters, debug=True, timeout=3)
intercept_connection.connect(self.mock_dcs_socket_file)
while True:
# Wait for a code to arrive
diff --git a/tests/test_object_model.py b/tests/test_object_model.py
index 51012e2..cf1381c 100644
--- a/tests/test_object_model.py
+++ b/tests/test_object_model.py
@@ -4,8 +4,19 @@
from typing import Optional, cast
from src.dsf.object_model import (
- Accelerometer, Boards, BoardState, DriverId, ExpansionBoard, Heater, InputChannel, MainBoard, ObjectModel, Plugin,
- ProbeLoadCell, ProbeType)
+ Accelerometer,
+ Boards,
+ BoardState,
+ DriverId,
+ ExpansionBoard,
+ Heater,
+ InputChannel,
+ MainBoard,
+ ObjectModel,
+ Plugin,
+ ProbeLoadCell,
+ ProbeType,
+)
from src.dsf.object_model.utils import is_model_object, JSONElement, JSONObj, model_prop, nullable_model_prop
from src.dsf.object_model.object_model import ModelCollection, ModelDictionary, ModelObject
@@ -29,18 +40,22 @@ class Dummy(ModelObject):
np_model = nullable_model_prop("np_model", SubModel)
p_model_collection = model_prop("p_model_collection", ModelCollection[SubModel], ModelCollection(SubModel))
- p_model_ncollection = model_prop("p_model_ncollection", ModelCollection[Optional[SubModel]], ModelCollection(Optional[SubModel]))
+ p_model_ncollection = model_prop(
+ "p_model_ncollection", ModelCollection[Optional[SubModel]], ModelCollection(Optional[SubModel])
+ )
np_model_collection = nullable_model_prop(
- "np_model_collection", ModelCollection[SubModel], lambda: ModelCollection(SubModel))
+ "np_model_collection", ModelCollection[SubModel], lambda: ModelCollection(SubModel)
+ )
np_model_ncollection = nullable_model_prop(
- "np_model_ncollection", ModelCollection[Optional[SubModel]], lambda: ModelCollection(Optional[SubModel]))
-
+ "np_model_ncollection", ModelCollection[Optional[SubModel]], lambda: ModelCollection(Optional[SubModel])
+ )
+
def setUp(self):
pass
def tearDown(self):
pass
-
+
def test_update_from_json(self):
model = self.Dummy()
@@ -52,15 +67,15 @@ def test_update_from_json(self):
self.assertIsNone(model.np_str)
patch: JSONObj = {
- 'p_int': 10,
- 'np_int': 11,
- 'p_float': 1.1,
- 'np_float': 1.2,
- 'p_str': "hello",
- 'np_str': "world"
+ "p_int": 10,
+ "np_int": 11,
+ "p_float": 1.1,
+ "np_float": 1.2,
+ "p_str": "hello",
+ "np_str": "world",
}
model.update_from_json(patch)
-
+
self.assertEqual(model.p_int, 10)
self.assertEqual(model.np_int, 11)
self.assertEqual(model.p_float, 1.1)
@@ -73,36 +88,40 @@ def test_update_from_json_bad_data(self):
# Test non nullable props
- self.assertRaises(ValueError, lambda: model.update_from_json({'p_int': "not an int"}))
- self.assertRaises(TypeError, lambda: model.update_from_json({'p_int': None}))
- self.assertEqual(model.update_from_json({'p_int': 1.1}).p_int, 1) # float should be cast to int
- self.assertEqual(model.update_from_json({'p_int': "2"}).p_int, 2) # str should be cast to int
+ self.assertRaises(ValueError, lambda: model.update_from_json({"p_int": "not an int"}))
+ self.assertRaises(TypeError, lambda: model.update_from_json({"p_int": None}))
+ self.assertEqual(model.update_from_json({"p_int": 1.1}).p_int, 1) # float should be cast to int
+ self.assertEqual(model.update_from_json({"p_int": "2"}).p_int, 2) # str should be cast to int
- self.assertRaises(ValueError, lambda: model.update_from_json({'p_float': "not a float"}))
- self.assertRaises(TypeError, lambda: model.update_from_json({'p_float': None}))
- self.assertEqual(model.update_from_json({'p_float': 2}).p_float, 2.0) # int should be cast to float
- self.assertEqual(model.update_from_json({'p_float': "3.14"}).p_float, 3.14) # str should be cast to float
+ self.assertRaises(ValueError, lambda: model.update_from_json({"p_float": "not a float"}))
+ self.assertRaises(TypeError, lambda: model.update_from_json({"p_float": None}))
+ self.assertEqual(model.update_from_json({"p_float": 2}).p_float, 2.0) # int should be cast to float
+ self.assertEqual(model.update_from_json({"p_float": "3.14"}).p_float, 3.14) # str should be cast to float
- self.assertRaises(TypeError, lambda: model.update_from_json({'p_str': None}))
- self.assertEqual(model.update_from_json({'p_str': 123}).p_str, "123") # int should be cast to str
- self.assertEqual(model.update_from_json({'p_str': 3.14}).p_str, "3.14") # float should be cast to str
+ self.assertRaises(TypeError, lambda: model.update_from_json({"p_str": None}))
+ self.assertEqual(model.update_from_json({"p_str": 123}).p_str, "123") # int should be cast to str
+ self.assertEqual(model.update_from_json({"p_str": 3.14}).p_str, "3.14") # float should be cast to str
# Test nullable props
- self.assertRaises(ValueError, lambda: model.update_from_json({'np_int': "not an int"}))
- self.assertEqual(model.update_from_json({'np_int': None}).np_int, None) # nullable prop should be set to None
- self.assertEqual(model.update_from_json({'np_int': 1.1}).np_int, 1) # float should be cast to int
- self.assertEqual(model.update_from_json({'np_int': "2"}).np_int, 2) # str should be cast to int
-
- self.assertRaises(ValueError, lambda: model.update_from_json({'np_float': "not a float"}))
- self.assertEqual(model.update_from_json({'np_float': None}).np_float, None) # nullable prop should be set to None
- self.assertEqual(model.update_from_json({'np_float': 2}).np_float, 2.0) # int should be cast to float
- self.assertEqual(model.update_from_json({'np_float': "3.14"}).np_float, 3.14) # str should be cast to float
-
- self.assertEqual(model.update_from_json({'np_str': None}).np_str, None) # nullable prop should be set to None
- self.assertEqual(model.update_from_json({'np_str': 123}).np_str, "123") # int should be cast to str
- self.assertEqual(model.update_from_json({'np_str': 3.14}).np_str, "3.14") # float should be cast to str
- self.assertEqual(model.update_from_json({'np_str': "hello"}).np_str, "hello") # str should be accepted for nullable prop
+ self.assertRaises(ValueError, lambda: model.update_from_json({"np_int": "not an int"}))
+ self.assertEqual(model.update_from_json({"np_int": None}).np_int, None) # nullable prop should be set to None
+ self.assertEqual(model.update_from_json({"np_int": 1.1}).np_int, 1) # float should be cast to int
+ self.assertEqual(model.update_from_json({"np_int": "2"}).np_int, 2) # str should be cast to int
+
+ self.assertRaises(ValueError, lambda: model.update_from_json({"np_float": "not a float"}))
+ self.assertEqual(
+ model.update_from_json({"np_float": None}).np_float, None
+ ) # nullable prop should be set to None
+ self.assertEqual(model.update_from_json({"np_float": 2}).np_float, 2.0) # int should be cast to float
+ self.assertEqual(model.update_from_json({"np_float": "3.14"}).np_float, 3.14) # str should be cast to float
+
+ self.assertEqual(model.update_from_json({"np_str": None}).np_str, None) # nullable prop should be set to None
+ self.assertEqual(model.update_from_json({"np_str": 123}).np_str, "123") # int should be cast to str
+ self.assertEqual(model.update_from_json({"np_str": 3.14}).np_str, "3.14") # float should be cast to str
+ self.assertEqual(
+ model.update_from_json({"np_str": "hello"}).np_str, "hello"
+ ) # str should be accepted for nullable prop
def test_update_from_json_model_object(self):
model = self.Dummy()
@@ -110,25 +129,20 @@ def test_update_from_json_model_object(self):
self.assertEqual(model.p_model.value, 0)
self.assertIsNone(model.np_model)
- patch: JSONObj = {
- 'p_model': {
- 'value': 10
- },
- 'np_model': {
- 'value': 20
- }
- }
+ patch: JSONObj = {"p_model": {"value": 10}, "np_model": {"value": 20}}
model.update_from_json(patch)
self.assertEqual(model.p_model.value, 10)
assert model.np_model is not None
self.assertEqual(model.np_model.value, 20)
- model.update_from_json({'np_model': None})
+ model.update_from_json({"np_model": None})
self.assertIsNone(model.np_model)
- self.assertRaises(TypeError, lambda: model.update_from_json({'p_model': None})) # non nullable model prop should not accept None
-
+ self.assertRaises(
+ TypeError, lambda: model.update_from_json({"p_model": None})
+ ) # non nullable model prop should not accept None
+
def test_update_from_json_model_collection(self):
model = self.Dummy()
@@ -136,10 +150,10 @@ def test_update_from_json_model_collection(self):
self.assertIsNone(model.np_model_collection)
patch: JSONObj = {
- 'p_model_collection': [{}],
- 'p_model_ncollection': [{}, None],
- 'np_model_collection': [{}],
- 'np_model_ncollection': [{}, None],
+ "p_model_collection": [{}],
+ "p_model_ncollection": [{}, None],
+ "np_model_collection": [{}],
+ "np_model_ncollection": [{}, None],
}
model.update_from_json(patch)
@@ -155,15 +169,25 @@ def test_update_from_json_model_collection(self):
self.assertIsNotNone(model.p_model_ncollection[0])
self.assertIsNone(model.p_model_ncollection[1])
- model.update_from_json({'p_model_collection': [{'value': 10}, {'value': 20}], 'np_model_collection': [{'value': 30}, {'value': 40}]})
+ model.update_from_json(
+ {
+ "p_model_collection": [{"value": 10}, {"value": 20}],
+ "np_model_collection": [{"value": 30}, {"value": 40}],
+ }
+ )
self.assertEqual(len(model.p_model_collection), 2)
self.assertEqual(model.p_model_collection[0].value, 10)
self.assertEqual(model.p_model_collection[1].value, 20)
self.assertEqual(len(model.np_model_collection), 2)
self.assertEqual(model.np_model_collection[0].value, 30)
self.assertEqual(model.np_model_collection[1].value, 40)
-
- model.update_from_json({'p_model_ncollection': [{'value': 30}, None, {'value': 40}], 'np_model_ncollection': [{'value': 50}, None, {'value': 60}]})
+
+ model.update_from_json(
+ {
+ "p_model_ncollection": [{"value": 30}, None, {"value": 40}],
+ "np_model_ncollection": [{"value": 50}, None, {"value": 60}],
+ }
+ )
self.assertEqual(len(model.p_model_ncollection), 3)
p_first, p_second, p_third = model.p_model_ncollection
assert p_first is not None and p_third is not None
@@ -177,25 +201,28 @@ def test_update_from_json_model_collection(self):
self.assertIsNone(np_second)
self.assertEqual(np_third.value, 60)
- model.update_from_json({'p_model_ncollection': [None], 'np_model_ncollection': [None]})
+ model.update_from_json({"p_model_ncollection": [None], "np_model_ncollection": [None]})
self.assertEqual(len(model.p_model_ncollection), 1)
self.assertIsNone(model.p_model_ncollection[0])
self.assertEqual(len(model.np_model_ncollection), 1)
self.assertIsNone(model.np_model_ncollection[0])
- model.update_from_json({'p_model_collection': [], 'p_model_ncollection': []})
+ model.update_from_json({"p_model_collection": [], "p_model_ncollection": []})
self.assertEqual(len(model.p_model_collection), 0)
self.assertEqual(len(model.p_model_ncollection), 0)
- self.assertRaises(TypeError, lambda: model.update_from_json({'p_model_collection': None})) # non nullable model collection should not accept None
- self.assertRaises(TypeError, lambda: model.update_from_json({'p_model_ncollection': None})) # non nullable model collection should not accept None
-
- model.update_from_json({'np_model_collection': None}) # nullable model collection should accept None
- model.update_from_json({'np_model_ncollection': None}) # nullable model collection should accept None
+ self.assertRaises(
+ TypeError, lambda: model.update_from_json({"p_model_collection": None})
+ ) # non nullable model collection should not accept None
+ self.assertRaises(
+ TypeError, lambda: model.update_from_json({"p_model_ncollection": None})
+ ) # non nullable model collection should not accept None
+
+ model.update_from_json({"np_model_collection": None}) # nullable model collection should accept None
+ model.update_from_json({"np_model_ncollection": None}) # nullable model collection should accept None
self.assertIsNone(model.np_model_collection)
self.assertIsNone(model.np_model_ncollection)
-
-
+
class TestModelCollection(unittest.TestCase):
def setUp(self):
@@ -209,7 +236,7 @@ def test_int_list(self):
model.update_from_json([1, 2, 3])
self.assertEqual(model, [1, 2, 3])
-
+
def test_nullable_int_list(self):
model: ModelCollection[Optional[int]] = ModelCollection(Optional[int])
@@ -240,7 +267,7 @@ def test_scalar_list_coerces_convertible_values(self):
self.assertEqual(model, [2, 3])
self.assertIsInstance(model[0], int)
self.assertIsInstance(model[1], int)
-
+
def test_model_object_list(self):
model: ModelCollection[Heater] = ModelCollection(Heater)
@@ -257,7 +284,7 @@ def test_model_object_list(self):
self.assertEqual(len(model), 1)
self.assertTrue(is_model_object(model[0]))
self.assertEqual(model[0].current, 30)
-
+
def test_nullable_model_object_list(self):
model: ModelCollection[Optional[Heater]] = ModelCollection(Optional[Heater])
@@ -277,7 +304,8 @@ def test_nullable_model_object_list(self):
self.assertEqual(len(model), 2)
self.assertIsNone(model[0])
self.assertTrue(is_model_object(model[1]))
-
+
+
class TestModelDictionary(unittest.TestCase):
class Dummy(ModelObject):
value = model_prop("value", int, 0)
@@ -302,14 +330,18 @@ def test_from_json(self):
def test_generic_dict(self):
model = ModelDictionary(False)
- model.update_from_json({"key1": 1, "key2": "hello", "key3": [1, 2, 3], "key4": {"nested": "dict"}, "key5": None})
+ model.update_from_json(
+ {"key1": 1, "key2": "hello", "key3": [1, 2, 3], "key4": {"nested": "dict"}, "key5": None}
+ )
self.assertEqual(model["key1"], 1)
self.assertEqual(model["key2"], "hello")
self.assertEqual(model["key3"], [1, 2, 3])
self.assertEqual(model["key4"], {"nested": "dict"})
self.assertIsNone(model["key5"])
- model.update_from_json({"key1": 2, "key2": "world", "key3": [4, 5], "key4": {"nested": "updated"}, "key5": "not null anymore"})
+ model.update_from_json(
+ {"key1": 2, "key2": "world", "key3": [4, 5], "key4": {"nested": "updated"}, "key5": "not null anymore"}
+ )
self.assertEqual(model["key1"], 2)
self.assertEqual(model["key2"], "world")
self.assertEqual(model["key3"], [4, 5])
@@ -326,12 +358,14 @@ def test_generic_dict(self):
def test_generic_non_nullable_dict(self):
model = ModelDictionary(True)
- model.update_from_json({"key1": 1, "key2": "hello", "key3": [1, 2, 3], "key4": {"nested": "dict"}, "key5": None})
+ model.update_from_json(
+ {"key1": 1, "key2": "hello", "key3": [1, 2, 3], "key4": {"nested": "dict"}, "key5": None}
+ )
self.assertEqual(model["key1"], 1)
- self.assertNotIn("key5", model) # non nullable dict should delete the key when set to null
+ self.assertNotIn("key5", model) # non nullable dict should delete the key when set to null
model.update_from_json({"key1": None})
- self.assertNotIn("key1", model) # non nullable dict should delete the key when set to null
+ self.assertNotIn("key1", model) # non nullable dict should delete the key when set to null
def test_model_object_dict(self):
model = ModelDictionary(True, self.Dummy)
@@ -343,13 +377,15 @@ def test_model_object_dict(self):
self.assertEqual(model["item2"].value, 20)
model.update_from_json({"item1": {"value": 30}})
- self.assertEqual(model["item1"].value, 30) # item1 should be updated instead of replaced
- self.assertEqual(model["item2"].value, 20) # item2 should not be altered
+ self.assertEqual(model["item1"].value, 30) # item1 should be updated instead of replaced
+ self.assertEqual(model["item2"].value, 20) # item2 should not be altered
model.update_from_json({"item1": None})
self.assertNotIn("item1", model)
- self.assertRaises(TypeError, lambda: model.update_from_json({"item1": 1})) # can't update a model object with a non-dict value
+ self.assertRaises(
+ TypeError, lambda: model.update_from_json({"item1": 1})
+ ) # can't update a model object with a non-dict value
class TestDriverId(unittest.TestCase):
@@ -423,8 +459,10 @@ def test_boards(self):
def test_boards_main_and_expansion(self):
model = ObjectModel()
- model.update_from_json('{"boards": [{"name": "Duet 3 MB6HC", "firmwareName": "RepRapFirmware", "maxHeaters": 32},'
- ' {"canAddress": 1, "name": "Duet 3 EXP3HC", "state": "timedOut", "timeout": 15}]}')
+ model.update_from_json(
+ '{"boards": [{"name": "Duet 3 MB6HC", "firmwareName": "RepRapFirmware", "maxHeaters": 32},'
+ ' {"canAddress": 1, "name": "Duet 3 EXP3HC", "state": "timedOut", "timeout": 15}]}'
+ )
self.assertIsInstance(model.boards, Boards)
main_board, expansion_board = model.boards
assert isinstance(main_board, MainBoard)
@@ -442,10 +480,12 @@ def test_boards_main_and_expansion(self):
def test_sensors_accelerometers_and_load_cell(self):
model = ObjectModel()
- model.update_from_json('{"sensors": {"accelerometers": [null, {"orientation": 25, "port": "121.spi.cs0",'
- ' "resolution": 16, "samplingRate": 1344}],'
- ' "probes": [{"type": 12, "loadCell": {"force": 12.5, "gramsPerCount": 0.01,'
- ' "preload": 50, "preloadWindow": [10, 100]}}]}}')
+ model.update_from_json(
+ '{"sensors": {"accelerometers": [null, {"orientation": 25, "port": "121.spi.cs0",'
+ ' "resolution": 16, "samplingRate": 1344}],'
+ ' "probes": [{"type": 12, "loadCell": {"force": 12.5, "gramsPerCount": 0.01,'
+ ' "preload": 50, "preloadWindow": [10, 100]}}]}}'
+ )
self.assertIsNone(model.sensors.accelerometers[0])
accelerometer = model.sensors.accelerometers[1]
assert isinstance(accelerometer, Accelerometer)
@@ -464,12 +504,14 @@ def test_rc2_fields(self):
from src.dsf.object_model.move.input_shaping import InputShapingType
model = ObjectModel()
- model.update_from_json('{"limits": {"reportedAxes": 9},'
- ' "move": {"minSpeed": 60, "usingSCurve": true, "currentMove": {"filePosition": 1234},'
- ' "axes": [{"phaseStep": true}], "shaping": {"type": "ei2"},'
- ' "motionSystems": [{"printingAcceleration": 3000, "userPosition": [1, 2, 3]}]},'
- ' "job": {"build": {"objects": [{"cancelled": true}]}},'
- ' "sbc": {"upgrade": {"message": "Installing packages", "progress": 0.5}}}')
+ model.update_from_json(
+ '{"limits": {"reportedAxes": 9},'
+ ' "move": {"minSpeed": 60, "usingSCurve": true, "currentMove": {"filePosition": 1234},'
+ ' "axes": [{"phaseStep": true}], "shaping": {"type": "ei2"},'
+ ' "motionSystems": [{"printingAcceleration": 3000, "userPosition": [1, 2, 3]}]},'
+ ' "job": {"build": {"objects": [{"cancelled": true}]}},'
+ ' "sbc": {"upgrade": {"message": "Installing packages", "progress": 0.5}}}'
+ )
self.assertEqual(model.limits.reported_axes, 9)
self.assertEqual(model.move.min_speed, 60)
self.assertTrue(model.move.using_S_curve)
@@ -500,33 +542,33 @@ def test_null_clears_dictionary(self):
def test_global(self):
# "global" is a Python keyword so the JSON key is exposed as ObjectModel.globals
- with open('tests/object_model/model_full.json') as fp:
+ with open("tests/object_model/model_full.json") as fp:
json_data = json.load(fp)
model = ObjectModel.from_json(json_data)
- self.assertEqual(dict(model.globals), json_data['global'])
- self.assertEqual(model.globals['daemonTick'], 250)
- self.assertEqual(model.globals['nozzleDiameters'], [0.6, 0.4])
- self.assertIsNone(model.globals['ret'])
+ self.assertEqual(dict(model.globals), json_data["global"])
+ self.assertEqual(model.globals["daemonTick"], 250)
+ self.assertEqual(model.globals["nozzleDiameters"], [0.6, 0.4])
+ self.assertIsNone(model.globals["ret"])
# Serialization converts "globals" back to "global"
serialized = json.loads(model.to_json())
- self.assertIn('global', serialized)
- self.assertNotIn('globals', serialized)
- self.assertEqual(serialized['global'], json_data['global'])
+ self.assertIn("global", serialized)
+ self.assertNotIn("globals", serialized)
+ self.assertEqual(serialized["global"], json_data["global"])
# Patch updates, adds and nulls variables (null does not delete global variables)
model.update_from_json('{"global":{"daemonTick":500,"newVar":"hello","debug":null}}')
- self.assertEqual(model.globals['daemonTick'], 500)
- self.assertEqual(model.globals['newVar'], "hello")
- self.assertIn('debug', model.globals)
- self.assertIsNone(model.globals['debug'])
- self.assertEqual(model.globals['lastTool'], -1)
+ self.assertEqual(model.globals["daemonTick"], 500)
+ self.assertEqual(model.globals["newVar"], "hello")
+ self.assertIn("debug", model.globals)
+ self.assertIsNone(model.globals["debug"])
+ self.assertEqual(model.globals["lastTool"], -1)
# Global variable names matching reserved keys are kept untouched
model.update_from_json('{"global":{"type":1,"global":2}}')
- self.assertEqual(model.globals['type'], 1)
- self.assertEqual(model.globals['global'], 2)
- self.assertNotIn('type_', model.globals)
+ self.assertEqual(model.globals["type"], 1)
+ self.assertEqual(model.globals["global"], 2)
+ self.assertNotIn("type_", model.globals)
# Setting the whole object to null clears it
model.update_from_json('{"global":null}')
@@ -547,33 +589,35 @@ def test_reserved_keys(self):
'{"job":{"file":{"thumbnails":[{"format":"qoi","height":48,"width":48}]}},'
'"move":{"axes":[{"letter":"X","max":336,"min":-20.2}],"shaping":{"type":"ei2"}},'
'"plugins":{"TestPlugin":{"id":"TestPlugin","license":"MIT"}},'
- '"state":{"messageBox":{"max":10.5,"min":-1.5,"message":"test"}}}')
+ '"state":{"messageBox":{"max":10.5,"min":-1.5,"message":"test"}}}'
+ )
self.assertEqual(model.job.file.thumbnails[0].format, ThumbnailInfoFormat.QOI)
self.assertEqual(model.move.axes[0].max, 336)
self.assertEqual(model.move.axes[0].min, -20.2)
self.assertEqual(model.move.shaping.type, InputShapingType.ei2)
- self.assertEqual(model.plugins['TestPlugin'].id, "TestPlugin")
- self.assertEqual(model.plugins['TestPlugin'].license, "MIT")
+ self.assertEqual(model.plugins["TestPlugin"].id, "TestPlugin")
+ self.assertEqual(model.plugins["TestPlugin"].license, "MIT")
assert model.state.message_box is not None
self.assertEqual(model.state.message_box.max, 10.5)
self.assertEqual(model.state.message_box.min, -1.5)
# Serialization uses the original JSON key names
serialized = json.loads(model.to_json())
- self.assertEqual(serialized['job']['file']['thumbnails'][0]['format'], "qoi")
- self.assertEqual(serialized['move']['axes'][0]['max'], 336)
- self.assertEqual(serialized['move']['axes'][0]['min'], -20.2)
- self.assertEqual(serialized['move']['shaping']['type'], "ei2")
- self.assertEqual(serialized['plugins']['TestPlugin']['id'], "TestPlugin")
- self.assertEqual(serialized['plugins']['TestPlugin']['license'], "MIT")
- self.assertEqual(serialized['state']['messageBox']['max'], 10.5)
- self.assertEqual(serialized['state']['messageBox']['min'], -1.5)
- for key in ('format_', 'id_', 'license_', 'max_', 'min_', 'type_'):
+ self.assertEqual(serialized["job"]["file"]["thumbnails"][0]["format"], "qoi")
+ self.assertEqual(serialized["move"]["axes"][0]["max"], 336)
+ self.assertEqual(serialized["move"]["axes"][0]["min"], -20.2)
+ self.assertEqual(serialized["move"]["shaping"]["type"], "ei2")
+ self.assertEqual(serialized["plugins"]["TestPlugin"]["id"], "TestPlugin")
+ self.assertEqual(serialized["plugins"]["TestPlugin"]["license"], "MIT")
+ self.assertEqual(serialized["state"]["messageBox"]["max"], 10.5)
+ self.assertEqual(serialized["state"]["messageBox"]["min"], -1.5)
+ for key in ("format_", "id_", "license_", "max_", "min_", "type_"):
self.assertNotIn(f'"{key}"', model.to_json())
def test_http_endpoints(self):
from src.dsf.object_model import HttpEndpointType
+
model = ObjectModel()
json_patch = '{"sbc":{"dsf":{"httpEndpoints":[{"endpointType":"GET","namespace":"ExecOnMcode","path":"getCmdList","isUploadRequest":false,"unixSocket":"/run/dsf/ExecOnMcode/getCmdList-GET.sock"}]}}}'
@@ -630,11 +674,11 @@ def test_inputs(self):
@staticmethod
def test_job():
model = ObjectModel()
- json_patch = '{"job":{"file":{"filament":[496.4],"fileName":"0:/gcodes/Veil_Token.gcode","generatedBy":"ideaMaker 4.2.1.5321, 2022-10-01 17:45:38 UTC\u002B0200","height":1.04,"lastModified":"2022-10-01T16:45:39+01:00","layerHeight":0.12,"numLayers":9,"printTime":798,"size":594195,"thumbnails":[{"data":"iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAUnklEQVR4nO3cfYwcZ30H8O/zzMy\u002B3d77nl\u002BuztmOA7bjGIOdBBJioDSEJkBLStOWCERTaKS2alWkqqioqpD6BxSVSlUFaktVggRUjdomBVIaikrA4Y9AYkhsY5vEdnz2\u002Bezz3u3e3b7OzPP8\u002BsfM7O5dfMmdb\u002B9s1O9HWu3u3e7s8/xmnmeefZ7fLEBERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERET0/5261gXoljuA7N5cbuADw8PDU8bc6Isa3eJ5W3MaY/NWxiyQ6Xy9o1D0gCPnguBwv8jzvzY5\u002BTMAZq3L\u002BdG\u002BvqFfHRjYXDVmm0Dv2p72bimFZswCfZ2vc4GL/a5TnPTDM0UTHBtynBe/cv78\u002BOPALABZyzI\u002BAKTev2XL1poxb96bzR4oGbPLCAqdr3GgZvIaxZrF\u002BIwJpozIsY1aTz5Vr8/86/R08SWguZZlXC8/bw1E/3ImM3ZTOl24v39ot6OlcNmYnSOOuzWj9Zhv7UBW62FHqfRKNhpYWw1FXihZ84ILPPfVmZmnD1UqZ14GGqsp7Ad7e3ftdDOFd/f37r0Qhrs3uu4tWmGbo9RmT6nMa2\u002Bho4wijcDKBJRMzBjzk4xS4yfqwZGvz5dOfKdeH19NObcBmXv7\u002B/f81tDQnlIY3j7ierc5Su1MKdW/ku0YEb8pMuMpVZwJzZmGmHP9jvPSdBiePV71L/1naer0c8AlAHY15V1P13MDcf9\u002B4\u002BgDe3PpA3PW7negtg05zphWylmPDw9FmpNheOT5pvmdv7h0/shy3vO5jRt/6U2Z7IMN4NYRx9ntKOWtdTkBwIgEZWtOOMDh5xuNRz5\u002B8eJTy3nfp0dGbtqTyf3TJs99i7vCTuVqWREzY8y4hfxUizo6EQTHHpqc\u002BBcA4Xp8/kq517oAS9kDFLbkMl/2Fby840IjGv9YCBwAapVtOxRBAKAR39cB1CBoCDAnghIkXYXcOuXhLQCW1UC2ZHMPV5X6jawCqgBcicqqASgV3etVlltE0ABQhyAQYD6qg1cS7J2B7J1PpXIAnlrOtvqy2V9sOM7bp0TgQpCOy\u002BcAXS\u002B3FUETQAA4Ruvt88D2WbHvuZzyghHgfy4DF1f1AWvkum0gBghqgPgAVDzkFgAi0c1CoBGdq6ObwALQEj03AHwAAQQmfh5K1BCa8d8CAUIIfCzdffnG9Cy3zGVryspxkZQ5OaQ6yy1xXRTaDd7Gz61Erw0A\u002BB3lDgD4EjWKMC53gKihLB6rNMKwvNzyVq3NDTiCGhS0ADXVjvXiciOJL64Q87jsFlHM/bicSYxriMrrx/FuoP1lTwA0oipel67bBnICmC4a89NhR7/RU\u002B3zhQPAU4AjUeG1UlFPF/d8RgFGJD4jALMiKCNuICra2dHWXv17btPaSSvybDkMv7ncMp8Kgie3KvX\u002BYa03uAAcpRZ8SudjF0AWQEopaChoCBwV1TIQgYFCFdFZbTbufTu3caX\u002BvGnMuZLvf3e55X252XxCtL4rB/XOfkcPZSQq81LnCgFajdbGdeiBQkZF\u002B8WN94UPgZXoW3pNAVURVACUICjLwjiE1k7NA9PLLfN6u\u002B6\u002BgxwAvAO5XOGegYH\u002BqtXbJ8Tcf3M285sbtO7zgNZBtFwighqAM2LxsrG1sshpUWjOh\u002BHlukgttPYylJpztJ4GcPbM/Pz4nO9PHWs2X7rKKqjtmczYrb29u/dlMjdWwvCmPq1vvzGVunVIqXQvFFIA1FXUowzBRSu4LNJ80fefvWzN0XwqdWSiWj014ftnj1YqJ3CVM1w3AKP7h4dvvjGdLvRovdUDsr6VMc/RhR7AS2t3REFcLbJjm\u002BPkB5RCdoV1AKLGXxZBSezseL35RI\u002BSxzamUse/MzNz\u002BUeVSum56\u002Bxsci0biP5gPr97fyY/ckdv9pZJP9zd7zqb\u002Bx29o2mlkFaq39N62cOb5QhFalVrjzTF/qDkm6c/O3n\u002B6R8Dl7u1/T1A6s6BgZvv7\u002B3fXxO7f4Pr7nCV2hHPWuW79TkA0BSpiMhkzdpTc8YeySh14ofV\u002Bol/n5k6fgQodetzHsrnR27v7d0/6qbuGXKdNztK7ct0uS6\u002BSCMUuewoTJVDe6Eu5uQG1z17rF4/81i5fOxMozF9Mvq6te7WvIE8DHj3jI7uKAP7tzjeLqtx0IXaup4zUktJZn\u002BKYfiZD54//7WVvv/RTZtGqlq/64ZU6h4ruKPguq9fi3KuVDEMT3gK3zvj\u002B9/7WbX635\u002BZnV1xg3l0dOxdQ57\u002BqyHXfdNalHElRMRMxzNfrqijE37wbI\u002BWo1\u002B\u002BcGH8G9FIes2sdQNRj90w9s\u002BjrvtAt88G3VASi6IITlv52Z\u002BdfXnnSt//lRtu\u002BPwOL/X76asYaqyHkrX2ULPx\u002BU9duPBHK33vf2y/8eSgUq/PAfCuw/oF1laPBsG3P3r\u002B3Aewhguna/4lfUphq6fQkxULr2MKUcU3Hb9OYeXj8oQVac2sJF8gQ8QzVALMQ1AXoBLfz8Tj\u002BdYMkFxdfC8DPSkRpOIp0lRcNzeup1rqdpX1FJFW/Vozc4jG9fMQ1AQoQ6L6Rd\u002B99LzIihb7EnPxdLKLaLo6paLHHgBn8VRw/J5u7c/F9UxmxxoiKMV1nRDpOWvtwFV9wAqsdQMRALUmgM7p2k4qntWIpjmjg9aJ3yhoTylK6z4Kni/tqcQQ0RRugKhRROsb0XRuGP9/LZZujbVVdGy/vey\u002BqJ6CBfNmItFkb3IwJfVLpj7b94IgXqdpN4aongGima1mXM8A3c2T6ayBRTQjFe3Dxf9d\u002BKYF84PS7oSS6WHE5Wzvz/Z0cT1\u002BfYB2Pf14Gr7WXkdpl0ut7fAKWIcziAAXjAhcqFZXI0vcTMe7kgCGglZDMPF/Og96A8BIcgBJ6\u002BzRebAsdX4wIjURGZ8Ngr\u002B5mrqVQ3my37W/3qvVBhfRYlpnp7m4fp0HSNLYk78paZfZR3JQLjwzBvFaQ4D2gfRqDd\u002BKzDZEnrqauh2u1z51UyrzibxWO3NaZVyJG/SiaWDpuL9SfTsbQVK/UBY3wPY\u002B9RGt\u002BST1TOp9pX3oiExi6d3bFWs\u002BuPxQobD59ZnM72aBd/c6zu6s1oMpAJl4ujM6dSt4aJ\u002Buk1tnkGvxesa0CBrxAZIsogFR0P24B/I7/t9asLK20rD2rIgUNXBo1pgTPY5z\u002BKnJyfFL0cL3Vflwf//2kVzu7hHPO\u002BiJ7MxovTOndX8a7d4nOVskS4QeoszJlFJIoz1USSR1DhD1qqV4LaQs7UXCEO0GlBxcgbWzDWvHHeCFWWO\u002BB2O\u002B//Vi8eTV1u2tQO8bN20aE\u002BC2Ta672wH2aqV29yldSDs676E9rExGAUl9BQpGBKJUqzNQAHoB5KGQUapV784GZeI61xEN8yoQXBDBXDxi8K2drxlzGMCTl\u002Br1r/1vuXz2auu3HGvaQA4A3tuGhzfcmU5vnbdq37Z06kBTyX0j2tmsV/F9Y0IEzwT\u002BibKVFw2AmjWXrUgQaF33gGLW8y7XwnDuzPz8eN3a2ulqdXwOmOlu7ZakCsCmsZ6ekd3Z7NimVHZsg6vu3KjUfdscd7AHK1/LSRgRXBLBuNj5s2H4jUlrf1D0/fGLtdrkbK127hQw1d2qLK0PGBpNpws78vmxbel0YcB1\u002B\u002BrGjAiQs9YOp5TyUsoZCWGRh9ryhpS3b1RpvZq6F62dTAv\u002BayoMn3PEPH\u002B60Rh/dGbm0lqunXS1gbwZ6Hvn0NCet\u002Bfzt9dE7dvkOfuMYDSr1Ei3p3RDkYuTgf/Xf3Du3BfORxkk14178/mRB3sHDuRd9Z4R1zvoKbWj2\u002BsgRqTZsPbEnLGHG7CP/\u002BW5c9//CbDsNJP18NG\u002BvqEHh4b\u002BNK/1RzylN3Vz21bENEWKSmFiJgyPOMDhI43Gc1\u002Bemnr\u002BGFDp1uesuIE8APQf3LCh8LpMZvOFwNxycyZ1c9nYPVmtd/U5zmi3CrZcgUhlKgwe88PwH57x/Rc\u002BWyyu64LSA\u002Bgbeu8v9Baswb4tae\u002BOQHDXJs\u002B9bT3LAAC\u002BtbU5Y486GofP\u002B/5TvSIvfHFycuJbwNx6luPhwcH\u002Bd\u002BRyB4Zc9z2DjvtwtzuG12JFTMmYs0m28LgfHO918OKpqpn8t/LFqedWuG7yqg3kvp6eTQezvbsOxivdoylvL4DNAApZrdd8im0lrIgJIadKoXlyyg8f\u002B8jFiaewBl/gfhvI3DC44cBbezNv9ZRzMK2wx9G6kFaqt9uftRqBtTUDTNStnAzEHDru\u002Bz/8\u002BMWLT2ON0sq/uGHLG0Yy\u002BmMbXfdeD2qrXqdU/\u002BUwIsa3tqyUnhJIsRSaIwOuPn6k0XjxC5OTP3y1zINWA3konx95b3//Dqv1XQPafbuI7Cx47uvWpwrd93LT/9svnh//xLe6cGXbh4Ge\u002BzZvuWc45dzf67j3p9e5V\u002ByWpkilFAZP1EJ8db42d\u002BihcnnVQ7J7gfTHbhj7zPZU6o\u002B7UcZroWzMORE8M2PDZ7S1T39zdvbUlyqVywCgPjcy8qa9ufwnc1rdntF6VF/HGb4rMWNt7dH5uYP/WCweXs12HkT/4MPbhh7vc5y3dats14M5a559ZGrqfY9Uq6u6DuOThcL\u002Bu3v7Dg1onetW2a4lC4QNay8YkR\u002BfrQefdm/L5w/ltXPdpYGsVF0ENQguWMEZsThvrb4osurGXs36\u002BWlgVzpeTV7thVrXmolTz58xduxH1qZWu72667qnxerh\u002BKL6rIqn7H9OY6UBN6f1GICxjVm5zT0vUtssticLhdR1mHNzJbMirfWBmTj9IE6taC\u002B8AalCOn1VaRaLzYlARJABkIUgHa/huD8n8ZqL4zMpFhPx44rtTm7B9lSqzwFSNURjWUcEHqKUlBQEXrzu4\u002BHnK15nrMXzYQi3Ya1f0hplCLREOUUu4guToOLLLV\u002BZO3W16xiJzvyp5JYs6vkiqMbpE1WJFglnEDWKOazDT49cqbyI0lf8jqvudHxJbZSf1M7BWpxr1o08rFY5pL0IKuiIGQRVARoQVASYE4uyANMdK9hrISlHolUmAZoq\u002BksrV0ukdWxpxBe8IYrb4uOs8wZ073hLshiCuJxVEdQRdRpFEUyLtFaNayJww45pL4v2gkK0uinxFXgLSVxZwcK8KWBhwDp3TAhBKFF\u002BUeelpBbR6rBBx6Ww8TaSlISw4z3LnZZqWnvyTLl8dJkvX1Idr0znaOWOoR1sif8TX526YGUZ6MhR6sg3S7aBjm0lj9sxlDjPbGH2QCjty4o7L3E1EqVu\u002BGgfDFeigBBa\u002BysIxRUdKhaP3VUYOTekZasDAK9yIAuSPLL28\u002Bg\u002Bjl5HLldnAuSrxa0zRakzncfGcVucVeHHcWtdvowkVenKl1477\u002Brr\u002B5VerW9cnGOzWGdOUCu1IU6YawCt\u002B7q0n1fjvzUQZdI247/XEe3YprRbchgXOAlIZ6Ozi54vWUaRWihyrBaGj5wulf7wB/X6hdd4y2s6FYZz\u002B3pytZyjb3cVeqIfMlje6LozVWZh3NrXlCex6oxbDVGMGgCqiIaOPhbGLdlOiKXz2paKmRWplcLwT75fKj298ogsdDwIKi/Ozn5pIJud11oPWaUKWsFpHeDLjBXwyv19pbglyYtJrJqS/M4AWpdZNxBdqpwM\u002BxqIGkYD7QZqlviszng1rP2u\u002BvPR0Z0DjvN3Gxz37nzHeaydebpwQ7Joo0nCXfL6zkS0zvV/v9XHtndq8trkBwCS1pskIobSfu\u002BVKiAiUjXmuKfU8Wnff8JT6vCpqamXXlhFbtVSPjQyctP2dPrBLPT9w67e3a9U2ot3vY6vdRcAUO2YXSl5r93LyYKzhpH24yQ\u002B0eOOJD8sjFv7\u002BvCFP0wh8fB0cRZzzZijxtqv1YPg248Xi891KzYdUr\u002B3efNeR\u002BQNo573bgXsH3Scm3Iq\u002BhEwpyOZU2FRzLAwbsArz6jJ8QZ0nDWk3evbjr8nI5XW8SYLE1zDeEvJGSTJb0u2VTfmmZlK5cFWg3jf8PBtr0ulDg647jt6tN6RgRrJajWgAc/rGGZ17ujWc5WcBheePtunK1mQeRpIu0EIol/ESM4incMvI9IIrZ0VpSqi1GQ1DMcNMO4AP51oNE6fKZWOn1\u002B/HKuWXmD4/Rs33jnqeXvSSr1lUDtbNWTAgxpOK/Q4SmkH0S\u002BsJGnenb15q9GoOCayuFG0h2wL47ZwqGkk6Q0lGffbhtiyAWpa62IlDE8KcHKm2Tx0qlY78VK9fn6tY7PYENC3t69v596enp2eUnuyWu/q17rgKFXwgMG0UjkPqtdRHUmbi\u002BKW/OrLgs4mPiCtLGwUUawUgrgBSEeMWo2j43hrAIFv7awAMzVrf\u002BIAh8/U6986VCq9ACy9kp69e3Bw5I09PQPlMNziAje6WhcKrru5bm1BATkLFFwRV5Te4Kjkug6B0npQK5WNfpoH4lsz2dm7ATIXiFQCEau0vqSUqqS1nq8bU5kNw/HAmFoITA6m00VrzPzp2dnZS/V65eXrLM9oEWdXb\u002B/A7nR6eGsqlZ81ZhTWjmQ9b6zfcQoudM634YhA9SlBj1LoVUr1JuNpA1Epx9mcNBYLII6btNL5xV4KAWOVqgowl9b6cihSK/n\u002BeN3asmg9vcF1X56oVksXwrB2tFot4trMZyzLRqBnWzY7ONrTk9ueTg8Vw7Cggc09WhdcIDfoeRsq1m50RWkL2egoaCXIi1J9UVJf1IV4UdxU0tEG1l60EBsPo5rW2ul4pbgIpWppxynONJvjVWOmoFR5IJ0\u002Beb5SmZ0ol2eu1XXvRERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERNe3/wOxabcDNmswcQAAAABJRU5ErkJggg==","format":"png","height":200,"offset":483,"size":7144,"width":200}]},"build":{"currentObject":-1,"m486Names":false,"m486Numbers":false,"objects":[]},"duration":0,"pauseDuration":0,"rawExtrusion":0,"warmUpDuration":0,"layers":[],"lastDuration":null}}'
+ json_patch = '{"job":{"file":{"filament":[496.4],"fileName":"0:/gcodes/Veil_Token.gcode","generatedBy":"ideaMaker 4.2.1.5321, 2022-10-01 17:45:38 UTC\u002b0200","height":1.04,"lastModified":"2022-10-01T16:45:39+01:00","layerHeight":0.12,"numLayers":9,"printTime":798,"size":594195,"thumbnails":[{"data":"iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAUnklEQVR4nO3cfYwcZ30H8O/zzMy\u002b3d77nl\u002buztmOA7bjGIOdBBJioDSEJkBLStOWCERTaKS2alWkqqioqpD6BxSVSlUFaktVggRUjdomBVIaikrA4Y9AYkhsY5vEdnz2\u002bezz3u3e3b7OzPP8\u002bsfM7O5dfMmdb\u002b9s1O9HWu3u3e7s8/xmnmeefZ7fLEBERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERET0/5261gXoljuA7N5cbuADw8PDU8bc6Isa3eJ5W3MaY/NWxiyQ6Xy9o1D0gCPnguBwv8jzvzY5\u002bTMAZq3L\u002bdG\u002bvqFfHRjYXDVmm0Dv2p72bimFZswCfZ2vc4GL/a5TnPTDM0UTHBtynBe/cv78\u002bOPALABZyzI\u002bAKTev2XL1poxb96bzR4oGbPLCAqdr3GgZvIaxZrF\u002bIwJpozIsY1aTz5Vr8/86/R08SWguZZlXC8/bw1E/3ImM3ZTOl24v39ot6OlcNmYnSOOuzWj9Zhv7UBW62FHqfRKNhpYWw1FXihZ84ILPPfVmZmnD1UqZ14GGqsp7Ad7e3ftdDOFd/f37r0Qhrs3uu4tWmGbo9RmT6nMa2\u002bho4wijcDKBJRMzBjzk4xS4yfqwZGvz5dOfKdeH19NObcBmXv7\u002b/f81tDQnlIY3j7ierc5Su1MKdW/ku0YEb8pMuMpVZwJzZmGmHP9jvPSdBiePV71L/1naer0c8AlAHY15V1P13MDcf9\u002b4\u002bgDe3PpA3PW7negtg05zphWylmPDw9FmpNheOT5pvmdv7h0/shy3vO5jRt/6U2Z7IMN4NYRx9ntKOWtdTkBwIgEZWtOOMDh5xuNRz5\u002b8eJTy3nfp0dGbtqTyf3TJs99i7vCTuVqWREzY8y4hfxUizo6EQTHHpqc\u002bBcA4Xp8/kq517oAS9kDFLbkMl/2Fby840IjGv9YCBwAapVtOxRBAKAR39cB1CBoCDAnghIkXYXcOuXhLQCW1UC2ZHMPV5X6jawCqgBcicqqASgV3etVlltE0ABQhyAQYD6qg1cS7J2B7J1PpXIAnlrOtvqy2V9sOM7bp0TgQpCOy\u002bcAXS\u002b3FUETQAA4Ruvt88D2WbHvuZzyghHgfy4DF1f1AWvkum0gBghqgPgAVDzkFgAi0c1CoBGdq6ObwALQEj03AHwAAQQmfh5K1BCa8d8CAUIIfCzdffnG9Cy3zGVryspxkZQ5OaQ6yy1xXRTaDd7Gz61Erw0A\u002bB3lDgD4EjWKMC53gKihLB6rNMKwvNzyVq3NDTiCGhS0ADXVjvXiciOJL64Q87jsFlHM/bicSYxriMrrx/FuoP1lTwA0oipel67bBnICmC4a89NhR7/RU\u002b3zhQPAU4AjUeG1UlFPF/d8RgFGJD4jALMiKCNuICra2dHWXv17btPaSSvybDkMv7ncMp8Kgie3KvX\u002bYa03uAAcpRZ8SudjF0AWQEopaChoCBwV1TIQgYFCFdFZbTbufTu3caX\u002bvGnMuZLvf3e55X252XxCtL4rB/XOfkcPZSQq81LnCgFajdbGdeiBQkZF\u002b8WN94UPgZXoW3pNAVURVACUICjLwjiE1k7NA9PLLfN6u\u002b6\u002bgxwAvAO5XOGegYH\u002bqtXbJ8Tcf3M285sbtO7zgNZBtFwighqAM2LxsrG1sshpUWjOh\u002bHlukgttPYylJpztJ4GcPbM/Pz4nO9PHWs2X7rKKqjtmczYrb29u/dlMjdWwvCmPq1vvzGVunVIqXQvFFIA1FXUowzBRSu4LNJ80fefvWzN0XwqdWSiWj014ftnj1YqJ3CVM1w3AKP7h4dvvjGdLvRovdUDsr6VMc/RhR7AS2t3REFcLbJjm\u002bPkB5RCdoV1AKLGXxZBSezseL35RI\u002bSxzamUse/MzNz\u002bUeVSum56\u002bxsci0biP5gPr97fyY/ckdv9pZJP9zd7zqb\u002bx29o2mlkFaq39N62cOb5QhFalVrjzTF/qDkm6c/O3n\u002b6R8Dl7u1/T1A6s6BgZvv7\u002b3fXxO7f4Pr7nCV2hHPWuW79TkA0BSpiMhkzdpTc8YeySh14ofV\u002bol/n5k6fgQodetzHsrnR27v7d0/6qbuGXKdNztK7ct0uS6\u002bSCMUuewoTJVDe6Eu5uQG1z17rF4/81i5fOxMozF9Mvq6te7WvIE8DHj3jI7uKAP7tzjeLqtx0IXaup4zUktJZn\u002bKYfiZD54//7WVvv/RTZtGqlq/64ZU6h4ruKPguq9fi3KuVDEMT3gK3zvj\u002b9/7WbX635\u002bZnV1xg3l0dOxdQ57\u002bqyHXfdNalHElRMRMxzNfrqijE37wbI\u002bWo1\u002b\u002bcGH8G9FIes2sdQNRj90w9s\u002bjrvtAt88G3VASi6IITlv52Z\u002bdfXnnSt//lRtu\u002bPwOL/X76asYaqyHkrX2ULPx\u002bU9duPBHK33vf2y/8eSgUq/PAfCuw/oF1laPBsG3P3r\u002b3Aewhguna/4lfUphq6fQkxULr2MKUcU3Hb9OYeXj8oQVac2sJF8gQ8QzVALMQ1AXoBLfz8Tj\u002bdYMkFxdfC8DPSkRpOIp0lRcNzeup1rqdpX1FJFW/Vozc4jG9fMQ1AQoQ6L6Rd\u002b99LzIihb7EnPxdLKLaLo6paLHHgBn8VRw/J5u7c/F9UxmxxoiKMV1nRDpOWvtwFV9wAqsdQMRALUmgM7p2k4qntWIpjmjg9aJ3yhoTylK6z4Kni/tqcQQ0RRugKhRROsb0XRuGP9/LZZujbVVdGy/vey\u002bqJ6CBfNmItFkb3IwJfVLpj7b94IgXqdpN4aongGima1mXM8A3c2T6ayBRTQjFe3Dxf9d\u002bKYF84PS7oSS6WHE5Wzvz/Z0cT1\u002bfYB2Pf14Gr7WXkdpl0ut7fAKWIcziAAXjAhcqFZXI0vcTMe7kgCGglZDMPF/Og96A8BIcgBJ6\u002bzRebAsdX4wIjURGZ8Ngr\u002b5mrqVQ3my37W/3qvVBhfRYlpnp7m4fp0HSNLYk78paZfZR3JQLjwzBvFaQ4D2gfRqDd\u002bKzDZEnrqauh2u1z51UyrzibxWO3NaZVyJG/SiaWDpuL9SfTsbQVK/UBY3wPY\u002b9RGt\u002bST1TOp9pX3oiExi6d3bFWs\u002buPxQobD59ZnM72aBd/c6zu6s1oMpAJl4ujM6dSt4aJ\u002buk1tnkGvxesa0CBrxAZIsogFR0P24B/I7/t9asLK20rD2rIgUNXBo1pgTPY5z\u002bKnJyfFL0cL3Vflwf//2kVzu7hHPO\u002biJ7MxovTOndX8a7d4nOVskS4QeoszJlFJIoz1USSR1DhD1qqV4LaQs7UXCEO0GlBxcgbWzDWvHHeCFWWO\u002bB2O\u002b//Vi8eTV1u2tQO8bN20aE\u002bC2Ta672wH2aqV29yldSDs676E9rExGAUl9BQpGBKJUqzNQAHoB5KGQUapV784GZeI61xEN8yoQXBDBXDxi8K2drxlzGMCTl\u002br1r/1vuXz2auu3HGvaQA4A3tuGhzfcmU5vnbdq37Z06kBTyX0j2tmsV/F9Y0IEzwT\u002bibKVFw2AmjWXrUgQaF33gGLW8y7XwnDuzPz8eN3a2ulqdXwOmOlu7ZakCsCmsZ6ekd3Z7NimVHZsg6vu3KjUfdscd7AHK1/LSRgRXBLBuNj5s2H4jUlrf1D0/fGLtdrkbK127hQw1d2qLK0PGBpNpws78vmxbel0YcB1\u002b\u002brGjAiQs9YOp5TyUsoZCWGRh9ryhpS3b1RpvZq6F62dTAv\u002bayoMn3PEPH\u002b60Rh/dGbm0lqunXS1gbwZ6Hvn0NCet\u002bfzt9dE7dvkOfuMYDSr1Ei3p3RDkYuTgf/Xf3Du3BfORxkk14178/mRB3sHDuRd9Z4R1zvoKbWj2\u002bsgRqTZsPbEnLGHG7CP/\u002bW5c9//CbDsNJP18NG\u002bvqEHh4b\u002bNK/1RzylN3Vz21bENEWKSmFiJgyPOMDhI43Gc1\u002bemnr\u002bGFDp1uesuIE8APQf3LCh8LpMZvOFwNxycyZ1c9nYPVmtd/U5zmi3CrZcgUhlKgwe88PwH57x/Rc\u002bWyyu64LSA\u002bgbeu8v9Baswb4tae\u002bOQHDXJs\u002b9bT3LAAC\u002btbU5Y486GofP\u002b/5TvSIvfHFycuJbwNx6luPhwcH\u002bd\u002bRyB4Zc9z2DjvtwtzuG12JFTMmYs0m28LgfHO918OKpqpn8t/LFqedWuG7yqg3kvp6eTQezvbsOxivdoylvL4DNAApZrdd8im0lrIgJIadKoXlyyg8f\u002b8jFiaewBl/gfhvI3DC44cBbezNv9ZRzMK2wx9G6kFaqt9uftRqBtTUDTNStnAzEHDru\u002bz/8\u002bMWLT2ON0sq/uGHLG0Yy\u002bmMbXfdeD2qrXqdU/\u002bUwIsa3tqyUnhJIsRSaIwOuPn6k0XjxC5OTP3y1zINWA3konx95b3//Dqv1XQPafbuI7Cx47uvWpwrd93LT/9svnh//xLe6cGXbh4Ge\u002bzZvuWc45dzf67j3p9e5V\u002byWpkilFAZP1EJ8db42d\u002bihcnnVQ7J7gfTHbhj7zPZU6o\u002b7UcZroWzMORE8M2PDZ7S1T39zdvbUlyqVywCgPjcy8qa9ufwnc1rdntF6VF/HGb4rMWNt7dH5uYP/WCweXs12HkT/4MPbhh7vc5y3dats14M5a559ZGrqfY9Uq6u6DuOThcL\u002bu3v7Dg1onetW2a4lC4QNay8YkR\u002bfrQefdm/L5w/ltXPdpYGsVF0ENQguWMEZsThvrb4osurGXs36\u002bWlgVzpeTV7thVrXmolTz58xduxH1qZWu72667qnxerh\u002bKL6rIqn7H9OY6UBN6f1GICxjVm5zT0vUtssticLhdR1mHNzJbMirfWBmTj9IE6taC\u002b8AalCOn1VaRaLzYlARJABkIUgHa/huD8n8ZqL4zMpFhPx44rtTm7B9lSqzwFSNURjWUcEHqKUlBQEXrzu4\u002bHnK15nrMXzYQi3Ya1f0hplCLREOUUu4guToOLLLV\u002bZO3W16xiJzvyp5JYs6vkiqMbpE1WJFglnEDWKOazDT49cqbyI0lf8jqvudHxJbZSf1M7BWpxr1o08rFY5pL0IKuiIGQRVARoQVASYE4uyANMdK9hrISlHolUmAZoq\u002bksrV0ukdWxpxBe8IYrb4uOs8wZ073hLshiCuJxVEdQRdRpFEUyLtFaNayJww45pL4v2gkK0uinxFXgLSVxZwcK8KWBhwDp3TAhBKFF\u002bUeelpBbR6rBBx6Ww8TaSlISw4z3LnZZqWnvyTLl8dJkvX1Idr0znaOWOoR1sif8TX526YGUZ6MhR6sg3S7aBjm0lj9sxlDjPbGH2QCjty4o7L3E1EqVu\u002bGgfDFeigBBa\u002bysIxRUdKhaP3VUYOTekZasDAK9yIAuSPLL28\u002bg\u002bjl5HLldnAuSrxa0zRakzncfGcVucVeHHcWtdvowkVenKl1477\u002brr\u002b5VerW9cnGOzWGdOUCu1IU6YawCt\u002b7q0n1fjvzUQZdI247/XEe3YprRbchgXOAlIZ6Ozi54vWUaRWihyrBaGj5wulf7wB/X6hdd4y2s6FYZz\u002b3pytZyjb3cVeqIfMlje6LozVWZh3NrXlCex6oxbDVGMGgCqiIaOPhbGLdlOiKXz2paKmRWplcLwT75fKj298ogsdDwIKi/Ozn5pIJud11oPWaUKWsFpHeDLjBXwyv19pbglyYtJrJqS/M4AWpdZNxBdqpwM\u002bxqIGkYD7QZqlviszng1rP2u\u002bvPR0Z0DjvN3Gxz37nzHeaydebpwQ7Joo0nCXfL6zkS0zvV/v9XHtndq8trkBwCS1pskIobSfu\u002bVKiAiUjXmuKfU8Wnff8JT6vCpqamXXlhFbtVSPjQyctP2dPrBLPT9w67e3a9U2ot3vY6vdRcAUO2YXSl5r93LyYKzhpH24yQ\u002b0eOOJD8sjFv7\u002bvCFP0wh8fB0cRZzzZijxtqv1YPg248Xi891KzYdUr\u002b3efNeR\u002bQNo573bgXsH3Scm3Iq\u002bhEwpyOZU2FRzLAwbsArz6jJ8QZ0nDWk3evbjr8nI5XW8SYLE1zDeEvJGSTJb0u2VTfmmZlK5cFWg3jf8PBtr0ulDg647jt6tN6RgRrJajWgAc/rGGZ17ujWc5WcBheePtunK1mQeRpIu0EIol/ESM4incMvI9IIrZ0VpSqi1GQ1DMcNMO4AP51oNE6fKZWOn1\u002b/HKuWXmD4/Rs33jnqeXvSSr1lUDtbNWTAgxpOK/Q4SmkH0S\u002bsJGnenb15q9GoOCayuFG0h2wL47ZwqGkk6Q0lGffbhtiyAWpa62IlDE8KcHKm2Tx0qlY78VK9fn6tY7PYENC3t69v596enp2eUnuyWu/q17rgKFXwgMG0UjkPqtdRHUmbi\u002bKW/OrLgs4mPiCtLGwUUawUgrgBSEeMWo2j43hrAIFv7awAMzVrf\u002bIAh8/U6986VCq9ACy9kp69e3Bw5I09PQPlMNziAje6WhcKrru5bm1BATkLFFwRV5Te4Kjkug6B0npQK5WNfpoH4lsz2dm7ATIXiFQCEau0vqSUqqS1nq8bU5kNw/HAmFoITA6m00VrzPzp2dnZS/V65eXrLM9oEWdXb\u002b/A7nR6eGsqlZ81ZhTWjmQ9b6zfcQoudM634YhA9SlBj1LoVUr1JuNpA1Epx9mcNBYLII6btNL5xV4KAWOVqgowl9b6cihSK/n\u002beN3asmg9vcF1X56oVksXwrB2tFot4trMZyzLRqBnWzY7ONrTk9ueTg8Vw7Cggc09WhdcIDfoeRsq1m50RWkL2egoaCXIi1J9UVJf1IV4UdxU0tEG1l60EBsPo5rW2ul4pbgIpWppxynONJvjVWOmoFR5IJ0\u002beb5SmZ0ol2eu1XXvRERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERNe3/wOxabcDNmswcQAAAABJRU5ErkJggg==","format":"png","height":200,"offset":483,"size":7144,"width":200}]},"build":{"currentObject":-1,"m486Names":false,"m486Numbers":false,"objects":[]},"duration":0,"pauseDuration":0,"rawExtrusion":0,"warmUpDuration":0,"layers":[],"lastDuration":null}}'
model.update_from_json(json_patch)
def test_json_serialization(self):
- with open('tests/object_model/model_full.json') as fp:
+ with open("tests/object_model/model_full.json") as fp:
json_data = json.load(fp)
model = ObjectModel.from_json(json_data)
@@ -705,8 +749,14 @@ def test_move_kinematics(self):
def test_get_kinematics_type(self):
from src.dsf.object_model.move.kinematics import (
- CoreKinematics, DeltaKinematics, HangprinterKinematics, Kinematics, KinematicsName, PolarKinematics,
- ScaraKinematics)
+ CoreKinematics,
+ DeltaKinematics,
+ HangprinterKinematics,
+ Kinematics,
+ KinematicsName,
+ PolarKinematics,
+ ScaraKinematics,
+ )
expected_types = {
KinematicsName.cartesian: CoreKinematics,
@@ -735,18 +785,18 @@ def test_plugins(self):
self.assertEqual(len(model.plugins), 0)
# Plugin installation
- json_patch = '{"plugins":{"ExecOnMcode":{"dsfFiles":["execOnMcode.py","http_endpoints.py","MCodeAction.py","__init__.py"],"dwcFiles":["js/ExecOnMcode.09113059.js","js/ExecOnMcode.09113059.js.gz","js/ExecOnMcode.09113059.js.map","js/ExecOnMcode.09113059.js.map.gz"],"sdFiles":["sys/ExecOnMcode/top-example.py"],"pid":-1,"id":"ExecOnMcode","name":"ExecOnMcode","author":"Lo\u00EFc GRENON","version":"0.2","license":"GPL-3.0-or-later","homepage":"https://github.com/LoicGRENON/DSF_ExecOnMcode_Plugin","tags":[],"dwcVersion":"3.4.5","dwcDependencies":[],"sbcRequired":true,"sbcDsfVersion":"3.4.5","sbcExecutable":"execOnMcode.py","sbcExecutableArguments":null,"sbcExtraExecutables":[],"sbcOutputRedirected":true,"sbcPermissions":["commandExecution","codeInterceptionRead","registerHttpEndpoints","fileSystemAccess","launchProcesses"],"sbcPackageDependencies":[],"sbcPythonDependencies":["dsf-python\u003E=3.4.5"],"sbcPluginDependencies":[],"rrfVersion":null,"data":{}}}}'
+ json_patch = '{"plugins":{"ExecOnMcode":{"dsfFiles":["execOnMcode.py","http_endpoints.py","MCodeAction.py","__init__.py"],"dwcFiles":["js/ExecOnMcode.09113059.js","js/ExecOnMcode.09113059.js.gz","js/ExecOnMcode.09113059.js.map","js/ExecOnMcode.09113059.js.map.gz"],"sdFiles":["sys/ExecOnMcode/top-example.py"],"pid":-1,"id":"ExecOnMcode","name":"ExecOnMcode","author":"Lo\u00efc GRENON","version":"0.2","license":"GPL-3.0-or-later","homepage":"https://github.com/LoicGRENON/DSF_ExecOnMcode_Plugin","tags":[],"dwcVersion":"3.4.5","dwcDependencies":[],"sbcRequired":true,"sbcDsfVersion":"3.4.5","sbcExecutable":"execOnMcode.py","sbcExecutableArguments":null,"sbcExtraExecutables":[],"sbcOutputRedirected":true,"sbcPermissions":["commandExecution","codeInterceptionRead","registerHttpEndpoints","fileSystemAccess","launchProcesses"],"sbcPackageDependencies":[],"sbcPythonDependencies":["dsf-python\u003e=3.4.5"],"sbcPluginDependencies":[],"rrfVersion":null,"data":{}}}}'
model.update_from_json(json_patch)
self.assertEqual(len(model.plugins), 1)
- self.assertIsInstance(model.plugins.get('ExecOnMcode'), Plugin)
- self.assertEqual(len(model.plugins['ExecOnMcode'].dsf_files), 4)
- self.assertEqual(len(model.plugins['ExecOnMcode'].sbc_permissions), 5)
- self.assertEqual(model.plugins['ExecOnMcode'].pid, -1)
+ self.assertIsInstance(model.plugins.get("ExecOnMcode"), Plugin)
+ self.assertEqual(len(model.plugins["ExecOnMcode"].dsf_files), 4)
+ self.assertEqual(len(model.plugins["ExecOnMcode"].sbc_permissions), 5)
+ self.assertEqual(model.plugins["ExecOnMcode"].pid, -1)
# Plugin start
json_patch = '{"plugins":{"ExecOnMcode":{"pid":1125}}}'
model.update_from_json(json_patch)
- self.assertEqual(model.plugins['ExecOnMcode'].pid, 1125)
+ self.assertEqual(model.plugins["ExecOnMcode"].pid, 1125)
# Plugin removal
json_patch = '{"plugins":{"ExecOnMcode":null}}'
@@ -777,8 +827,12 @@ def test_sensors_filament_monitor(self):
def test_get_filament_monitor(self):
from src.dsf.object_model.sensors.filament_monitors import (
- FilamentMonitor, FilamentMonitorType, LaserFilamentMonitor, PulsedFilamentMonitor,
- RotatingMagnetFilamentMonitor)
+ FilamentMonitor,
+ FilamentMonitorType,
+ LaserFilamentMonitor,
+ PulsedFilamentMonitor,
+ RotatingMagnetFilamentMonitor,
+ )
expected_types = {
FilamentMonitorType.Laser: LaserFilamentMonitor,
@@ -800,8 +854,10 @@ def test_sensors_filament_monitor_rc2_fields(self):
from src.dsf.object_model.sensors.filament_monitors import RotatingMagnetFilamentMonitor
model = ObjectModel()
- model.update_from_json('{"sensors":{"filamentMonitors":[{"type":"rotatingMagnet","filamentPresent":true,"agc":120,'
- '"calibrated":{"mmPerRev":28.8,"percentMax":110,"percentMin":90,"totalDistance":100}}]}}')
+ model.update_from_json(
+ '{"sensors":{"filamentMonitors":[{"type":"rotatingMagnet","filamentPresent":true,"agc":120,'
+ '"calibrated":{"mmPerRev":28.8,"percentMax":110,"percentMin":90,"totalDistance":100}}]}}'
+ )
monitor = model.sensors.filament_monitors[0]
assert isinstance(monitor, RotatingMagnetFilamentMonitor)
self.assertTrue(monitor.filament_present)
@@ -826,5 +882,5 @@ def test_user_sessions(self):
self.assertEqual(model.sbc.dsf.user_sessions[0].session_type, SessionType.http)
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_send_simple_code.py b/tests/test_send_simple_code.py
index f6f9b6e..e0dd8bd 100644
--- a/tests/test_send_simple_code.py
+++ b/tests/test_send_simple_code.py
@@ -64,11 +64,13 @@ def _run_mock_dcs_server(self):
cmd_msg = conn.recv(1024)
expected_cmd = {
"command": "SimpleCode",
- "code": "echo \"Hello world!\"",
- "channel": "SBC", "executeAsynchronously": False
+ "code": 'echo "Hello world!"',
+ "channel": "SBC",
+ "executeAsynchronously": False,
}
- self.assertEqual(json.loads(cmd_msg.decode()), expected_cmd,
- f"Incorrect command message: {cmd_msg.decode()}")
+ self.assertEqual(
+ json.loads(cmd_msg.decode()), expected_cmd, f"Incorrect command message: {cmd_msg.decode()}"
+ )
# Send success response for the command
conn.sendall('{"result":"Hello world!", "success":true}'.encode())
@@ -86,7 +88,7 @@ def test_send_simple_code(self):
# res = command_connection.set_plugin_data("ExecOnMcode", "test", "1")
# Perform a simple command and wait for its output
- res = command_connection.perform_simple_code("echo \"Hello world!\"")
+ res = command_connection.perform_simple_code('echo "Hello world!"')
self.assertEqual(res, "Hello world!")
command_connection.close()
diff --git a/tests/test_socket_file.py b/tests/test_socket_file.py
index e613f53..472818a 100644
--- a/tests/test_socket_file.py
+++ b/tests/test_socket_file.py
@@ -33,5 +33,5 @@ def test_invalid_config_uses_default(self):
self.assertEqual(_read_socket_file(self.config_path), "/run/dsf/dcs.sock")
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_subscribe_object_model.py b/tests/test_subscribe_object_model.py
index efdfde9..80a637f 100644
--- a/tests/test_subscribe_object_model.py
+++ b/tests/test_subscribe_object_model.py
@@ -74,7 +74,7 @@ def _run_mock_dcs_server(self):
"mode": "Subscribe",
"version": PROTOCOL_VERSION,
"subscriptionMode": "Patch",
- "filters": []
+ "filters": [],
}
check_json(expected_setup, setup_msg.decode())
@@ -91,14 +91,15 @@ def _run_mock_dcs_server(self):
for model_file in model_updates:
# Send model data
- with open(model_file, 'r') as f:
+ with open(model_file, "r") as f:
update_data = json.load(f)
conn.sendall(json.dumps(update_data).encode())
# Verify acknowledge response
ack = conn.recv(1024)
- self.assertEqual(ack, self.acknowledge_response,
- f"Expected acknowledge command, received: {ack.decode()}")
+ self.assertEqual(
+ ack, self.acknowledge_response, f"Expected acknowledge command, received: {ack.decode()}"
+ )
# Keep the connection open until the client closes it, like DCS does
conn.settimeout(5)
diff --git a/tests/utils.py b/tests/utils.py
index 561396d..995710c 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -12,4 +12,6 @@ def check_json(expected_dict: Mapping[str, object], json_str: str) -> None:
if key not in json_obj:
raise AssertionError(f"Key '{key}' not found in JSON {json_str}")
if json_obj[key] != expected_value:
- raise AssertionError(f"Value for key '{key}' does not match. Expected: {expected_value}, Found: {json_obj[key]}")
\ No newline at end of file
+ raise AssertionError(
+ f"Value for key '{key}' does not match. Expected: {expected_value}, Found: {json_obj[key]}"
+ )
diff --git a/tox.ini b/tox.ini
index 47f618f..e45249f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@ envlist = py{311,312,313,314}
[testenv]
deps =
check-manifest
- black
+ black==26.5.1
pyright[nodejs]==1.1.414
flake8
pytest
From 9d837a0ef14394cdba6ead832c2da870caf4caa3 Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:20:01 +0100
Subject: [PATCH 13/19] style: fix flake8 issues
- Wrap long comments, docstrings and error messages
- Replace a bare except with except BaseException (same behaviour)
- Replace the star import in object_model/directories
- Mark AxisLetter.l as an intentional E741 exception
- Exempt deliberate late imports (E402) in dsf/__init__.py and
connections/__init__.py, and embedded JSON payloads in tests (E501)
---
setup.cfg | 6 ++++++
src/dsf/commands/code_parameter.py | 3 ++-
src/dsf/commands/object_model.py | 3 ++-
src/dsf/connections/base_command_connection.py | 6 ++++--
.../init_messages/client_init_messages.py | 3 ++-
src/dsf/http.py | 2 +-
src/dsf/object_model/boards/boards.py | 5 ++++-
src/dsf/object_model/directories/__init__.py | 2 +-
src/dsf/object_model/model_collection.py | 6 ++++--
src/dsf/object_model/move/axis.py | 2 +-
.../move/extruder_pressure_advance.py | 3 ++-
src/dsf/object_model/move/input_shaping.py | 3 ++-
src/dsf/object_model/sensors/accelerometer.py | 6 ++++--
src/dsf/object_model/utils.py | 15 ++++++++++-----
14 files changed, 45 insertions(+), 20 deletions(-)
diff --git a/setup.cfg b/setup.cfg
index 857db67..e1c088a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,6 +5,12 @@ license_files = LICENSE
[flake8]
max-line-length = 120
+per-file-ignores =
+ # Imports come after the definitions that submodules import back from the package (avoids circular imports)
+ src/dsf/__init__.py: E402
+ src/dsf/connections/__init__.py: E402
+ # Tests embed object model JSON payloads verbatim
+ tests/*: E501
[sdist_dsc]
# Force the version to use `~` before any prerelease identifiers
diff --git a/src/dsf/commands/code_parameter.py b/src/dsf/commands/code_parameter.py
index c6f163e..bad72ce 100644
--- a/src/dsf/commands/code_parameter.py
+++ b/src/dsf/commands/code_parameter.py
@@ -170,7 +170,8 @@ def _parse_expression_array(self) -> list[float]:
return [float(elem) for elem in elements if elem]
except ValueError as e:
raise Exception(
- f"Cannot parse expression array: failed to convert elements to numbers in {self.letter} (value {self.string_value})"
+ f"Cannot parse expression array: failed to convert elements to numbers in {self.letter}"
+ f" (value {self.string_value})"
) from e
def as_float_array(self) -> list[float]:
diff --git a/src/dsf/commands/object_model.py b/src/dsf/commands/object_model.py
index 0d5cfcd..9ff350d 100644
--- a/src/dsf/commands/object_model.py
+++ b/src/dsf/commands/object_model.py
@@ -55,7 +55,8 @@ def sync_object_model():
def set_wifi_country(country_code: Optional[str] = None):
"""
- Set the WiFi country code. This is a global setting on Linux, so it is applied to every WiFi interface in the object model
+ Set the WiFi country code. This is a global setting on Linux, so it is applied to every WiFi interface
+ in the object model
:param country_code: New WiFi country code, or null to clear it
"""
return BaseCommand("SetWifiCountry", **{"countryCode": country_code})
diff --git a/src/dsf/connections/base_command_connection.py b/src/dsf/connections/base_command_connection.py
index 00a74c5..3947413 100644
--- a/src/dsf/connections/base_command_connection.py
+++ b/src/dsf/connections/base_command_connection.py
@@ -191,7 +191,8 @@ def set_network_protocol(self, protocol: str, enabled: bool):
def set_wifi_country(self, country_code: Optional[str] = None):
"""
- Set the WiFi country code. This is a global setting on Linux, so it is applied to every WiFi interface in the object model
+ Set the WiFi country code. This is a global setting on Linux, so it is applied to every WiFi interface
+ in the object model
:param country_code: New WiFi country code, or null to clear it
"""
return self.perform_command(commands.object_model.set_wifi_country(country_code))
@@ -206,7 +207,8 @@ def set_update_status(self, is_updating: bool, message: str = "", progress: Opti
Override the current machine status if a software update is in progress
:param is_updating: Whether an update is now in progress
:param message: Description of the current update step, only used if is_updating is true
- :param progress: Progress of the current update step (0..1) or None if indeterminate, only used if is_updating is true
+ :param progress: Progress of the current update step (0..1) or None if indeterminate,
+ only used if is_updating is true
"""
res = self.perform_command(commands.generic.set_update_status(is_updating, message, progress))
return res.result
diff --git a/src/dsf/connections/init_messages/client_init_messages.py b/src/dsf/connections/init_messages/client_init_messages.py
index e6371bd..85dadc3 100644
--- a/src/dsf/connections/init_messages/client_init_messages.py
+++ b/src/dsf/connections/init_messages/client_init_messages.py
@@ -98,7 +98,8 @@ def subscribe_init_message(
The style of a filter is similar to XPath. For example, if you want to monitor only the current heater
temperatures, you can use the filter expression "heat/heaters[*]/current". Wildcards are supported either
for full names or indices. To get updates for an entire namespace, the ** wildcard can be used
- for example heat/** for everything heat-related), however it can be only used at the end of a filter expression.
+ for example heat/** for everything heat-related), however it can be only used at the end of a filter
+ expression.
Defaults to [].
verbose (bool): whether object model fields flagged as verbose are required.
obsolete (bool): whether object model fields flagged as obsolete are required.
diff --git a/src/dsf/http.py b/src/dsf/http.py
index c724490..1d6b92d 100644
--- a/src/dsf/http.py
+++ b/src/dsf/http.py
@@ -201,7 +201,7 @@ def _create_socket(self, path: str):
raise OSError(errno.EADDRINUSE, msg) from None
else:
raise
- except:
+ except BaseException:
sock.close()
raise
diff --git a/src/dsf/object_model/boards/boards.py b/src/dsf/object_model/boards/boards.py
index 8a13b88..e1437a1 100644
--- a/src/dsf/object_model/boards/boards.py
+++ b/src/dsf/object_model/boards/boards.py
@@ -132,7 +132,10 @@ def __init__(self):
class Boards(ModelCollection[Board]):
- """List of connected boards. The first board is the mainboard, every other one is an expansion board connected over CAN"""
+ """
+ List of connected boards.
+ The first board is the mainboard, every other one is an expansion board connected over CAN
+ """
def __init__(self, value: Optional[List[Board]] = None):
super(Boards, self).__init__(Board, value)
diff --git a/src/dsf/object_model/directories/__init__.py b/src/dsf/object_model/directories/__init__.py
index 6186f3f..ca5e9db 100644
--- a/src/dsf/object_model/directories/__init__.py
+++ b/src/dsf/object_model/directories/__init__.py
@@ -1,3 +1,3 @@
-from .directories import *
+from .directories import Directories
__all__ = ["Directories"]
diff --git a/src/dsf/object_model/model_collection.py b/src/dsf/object_model/model_collection.py
index 057bddd..732a942 100644
--- a/src/dsf/object_model/model_collection.py
+++ b/src/dsf/object_model/model_collection.py
@@ -63,7 +63,8 @@ def __init__(
ref_item = self._create_item(index)
if not is_model_object(ref_item):
raise TypeError(
- f"Item constructor for ModelCollection must inherit from type ModelType to update from a dict."
+ f"Item constructor for ModelCollection must inherit from type ModelType"
+ f" to update from a dict."
f" Got {type(ref_item).__name__}: {ref_item}"
)
# if issubclass(self._item_constructor, ModelType[T]):
@@ -143,7 +144,8 @@ def update_from_json(self, data: list[JSONElement]) -> "ModelCollection[T]":
self[i] = cast(T, ref_item.update_from_json(new_item_data))
else:
self[i] = self._coerce_item_value(new_item_data)
- # Use the `update_from_json` method of the current item if it's a model object, otherwise replace it with the new data
+ # Use the `update_from_json` method of the current item if it's a model object,
+ # otherwise replace it with the new data
elif is_model_object(current_item):
self[i] = cast(T, current_item.update_from_json(new_item_data))
else:
diff --git a/src/dsf/object_model/move/axis.py b/src/dsf/object_model/move/axis.py
index a6b6a05..e750f11 100644
--- a/src/dsf/object_model/move/axis.py
+++ b/src/dsf/object_model/move/axis.py
@@ -31,7 +31,7 @@ class AxisLetter(str, Enum):
i = "i"
j = "j"
k = "k"
- l = "l"
+ l = "l" # noqa: E741
m = "m"
n = "n"
o = "o"
diff --git a/src/dsf/object_model/move/extruder_pressure_advance.py b/src/dsf/object_model/move/extruder_pressure_advance.py
index 74c02fb..975331b 100644
--- a/src/dsf/object_model/move/extruder_pressure_advance.py
+++ b/src/dsf/object_model/move/extruder_pressure_advance.py
@@ -5,7 +5,8 @@
class ExtruderPressureAdvance(ModelObject):
"""Information about an extruder drive"""
- # Delay coefficient (in ms), or null if pressure advance is in simple mode (k0 = 0) - RRF reports infinity here as null
+ # Delay coefficient (in ms), or null if pressure advance is in simple mode (k0 = 0)
+ # RRF reports infinity here as null
d = nullable_model_prop("d", float)
# K0 coefficient
diff --git a/src/dsf/object_model/move/input_shaping.py b/src/dsf/object_model/move/input_shaping.py
index e9129d1..14e1000 100644
--- a/src/dsf/object_model/move/input_shaping.py
+++ b/src/dsf/object_model/move/input_shaping.py
@@ -61,7 +61,8 @@ class InputShaping(ModelObject):
# Frequency (in Hz)
frequency = model_prop("frequency", float, 40.0)
- # Minimum fraction of the original acceleration or feed rate to which the acceleration or feed rate may be reduced in order to apply input shaping
+ # Minimum fraction of the original acceleration or feed rate to which the acceleration or feed rate
+ # may be reduced in order to apply input shaping
reduction_limit = model_prop("reduction_limit", float, 0.25)
# Configured input shaping type
diff --git a/src/dsf/object_model/sensors/accelerometer.py b/src/dsf/object_model/sensors/accelerometer.py
index c0674c2..c6ad14a 100644
--- a/src/dsf/object_model/sensors/accelerometer.py
+++ b/src/dsf/object_model/sensors/accelerometer.py
@@ -9,13 +9,15 @@ class Accelerometer(ModelObject):
"""
# Orientation of the accelerometer
- # See https://docs.duet3d.com/en/Duet3D_hardware/Accessories/Duet3D_Accelerometer#orientation for a list of orientations
+ # See the following page for a list of orientations:
+ # https://docs.duet3d.com/en/Duet3D_hardware/Accessories/Duet3D_Accelerometer#orientation
orientation = model_prop("orientation", int, 20)
# Number of collected data points in the last run or 0 if it failed
points = model_prop("points", int)
- # Port name(s) the accelerometer is connected to as passed to M955 C, including the CAN address prefix on expansion boards
+ # Port name(s) the accelerometer is connected to as passed to M955 C,
+ # including the CAN address prefix on expansion boards
port = model_prop("port", str, "")
# Resolution the accelerometer is programmed for (in bits) or 0 if unknown
diff --git a/src/dsf/object_model/utils.py b/src/dsf/object_model/utils.py
index cf01fae..a16769e 100644
--- a/src/dsf/object_model/utils.py
+++ b/src/dsf/object_model/utils.py
@@ -38,7 +38,8 @@ def _set_model_prop(
elif isinstance(value, dict): # Update from JSON
if not isinstance(current_value, (ModelObject, ModelDictionary)):
raise TypeError(
- f"{instance.__class__.__name__}.{name} must be of type ModelObject or ModelDictionary to update from a dict."
+ f"{instance.__class__.__name__}.{name} must be of type ModelObject or ModelDictionary"
+ f" to update from a dict."
f" Got {type(current_value).__name__}: {current_value}"
)
current_value.update_from_json(cast(JSONObj, value))
@@ -55,14 +56,16 @@ def _set_model_prop(
setattr(instance, name, parsed_date)
except ValueError:
raise TypeError(
- f"{instance.__class__.__name__}.{name} must be a valid ISO format datetime string to update from JSON. Got: {value}"
+ f"{instance.__class__.__name__}.{name} must be a valid ISO format datetime string to update from JSON."
+ f" Got: {value}"
)
elif isinstance(value, (str, int, float, bool)):
converter = cast(Callable[[object], object], runtime_type)
setattr(instance, name, converter(value))
else:
raise TypeError(
- f"{instance.__class__.__name__}.{name} must be of type {runtime_type} or a compatible JSON element to update from."
+ f"{instance.__class__.__name__}.{name} must be of type {runtime_type}"
+ f" or a compatible JSON element to update from."
f" Got {type(value).__name__}: {value}"
)
@@ -72,7 +75,8 @@ def model_prop(name: str, model_type: type[T], default: Optional[T] = None) -> T
Wrap a none nullable model object property so that type checks can be performed during update
:param name: Property of the derived class
:param model_type: Constructor for creating new elements
- :param default: Default value of type to use if not set during update from JSON, if None then the default constructor of the model is used
+ :param default: Default value of type to use if not set during update from JSON,
+ if None then the default constructor of the model is used
:return:
"""
@@ -138,7 +142,8 @@ def nullable_model_prop(
model_type() # validate that the default constructor works
except Exception:
raise TypeError(
- f"Default constructor failed for type {model_type_name}. Provide a constructor function to create default values for the property {name}."
+ f"Default constructor failed for type {model_type_name}."
+ f" Provide a constructor function to create default values for the property {name}."
)
_factory: Callable[[], Optional[T]] = model_type
else:
From 81343b2b69c3b6debce1fbe791c2048bd7d8e7e7 Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:34:18 +0100
Subject: [PATCH 14/19] ci: enforce black and flake8, fix check-manifest
- Run black --check and the full flake8 config in CI, matching tox
- Pin black in requirements.txt
- Move the check-manifest ignore list to setup.cfg and ignore
development-only files so tox passes
- Add .git-blame-ignore-revs for the black reformat commit
---
.git-blame-ignore-revs | 5 +++++
.github/workflows/python-test.yml | 8 ++++----
requirements.txt | 1 +
setup.cfg | 14 ++++++++++++++
tox.ini | 2 +-
5 files changed, 25 insertions(+), 5 deletions(-)
create mode 100644 .git-blame-ignore-revs
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 0000000..11c0007
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,5 @@
+# Commits to skip in `git blame` (GitHub uses this file automatically)
+# Locally: git config blame.ignoreRevsFile .git-blame-ignore-revs
+
+# style: format code with black
+425a2ffa431b407684bea0214c20772f1ad36204
diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml
index 72957fc..6a2af7e 100644
--- a/.github/workflows/python-test.yml
+++ b/.github/workflows/python-test.yml
@@ -31,12 +31,12 @@ jobs:
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+ - name: Check formatting with black
+ run: |
+ black --check .
- name: Lint with flake8
run: |
- # stop the build if there are Python syntax errors or undefined names
- flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+ flake8 src examples tests --count --show-source --statistics
- name: Type check with pyright
run: |
pyright
diff --git a/requirements.txt b/requirements.txt
index 52a8fb1..e375668 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,3 +6,4 @@ bump2version
stdeb
requests
pyright[nodejs]==1.1.414
+black==26.5.1
diff --git a/setup.cfg b/setup.cfg
index e1c088a..2429e3b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -12,6 +12,20 @@ per-file-ignores =
# Tests embed object model JSON payloads verbatim
tests/*: E501
+[check-manifest]
+# Development-only files that are not part of the source distribution
+ignore =
+ .bumpversion.cfg
+ .git-blame-ignore-revs
+ .vscode/**
+ Makefile
+ conftest.py
+ docs/**
+ examples/**
+ requirements.txt
+ tests/**
+ tox.ini
+
[sdist_dsc]
# Force the version to use `~` before any prerelease identifiers
forced-upstream-version = 3.7.0-beta.1
diff --git a/tox.ini b/tox.ini
index e45249f..05b23d8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,7 +9,7 @@ deps =
flake8
pytest
commands =
- check-manifest --ignore 'tox.ini,docs/**,examples/**,tests/**'
+ check-manifest
python setup.py check -m -s
black --check --line-length 120 .
pyright
From 119c86496b47fa87c89f8e4f60c45048ae72cc63 Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:49:49 +0100
Subject: [PATCH 15/19] chore: skip missing interpreters in tox
---
tox.ini | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tox.ini b/tox.ini
index 05b23d8..8a03a98 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,7 @@
[tox]
envlist = py{311,312,313,314}
+# Environments whose Python version is not installed are skipped instead of failing
+skip_missing_interpreters = true
[testenv]
deps =
From f1a86cb65902035ed9dac32bc23f22529e43becd Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Tue, 29 Sep 2026 20:53:42 +0100
Subject: [PATCH 16/19] fix: don't use deprecate classifier
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 853e4f3..d0a56c2 100644
--- a/setup.py
+++ b/setup.py
@@ -13,11 +13,11 @@
url="https://github.com/Duet3D/dsf-python",
author="Duet3D Ltd.",
author_email="pkg@duet3d.com",
+ license="LGPL-3.0-or-later",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
- "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
From 0bd08f106acdbea23b29ede399ac95404f906d49 Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Wed, 30 Sep 2026 09:19:15 +0100
Subject: [PATCH 17/19] fiix: process last packet before closing connection
---
src/dsf/connections/base_connection.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/dsf/connections/base_connection.py b/src/dsf/connections/base_connection.py
index b817ad4..1b210c5 100644
--- a/src/dsf/connections/base_connection.py
+++ b/src/dsf/connections/base_connection.py
@@ -152,13 +152,16 @@ def receive_json(self) -> str:
# Refill the buffer and check again
BUFF_SIZE = 4096 # 4 KiB
data = b""
+ closed = False
while True:
try:
part = self.socket.recv(BUFF_SIZE)
except socket.timeout:
break
if not part:
- raise ConnectionError("Connection closed by the server")
+ # Parse what was already received before reporting the closed connection
+ closed = True
+ break
data += part
# end of the currently available data
if len(part) < BUFF_SIZE:
@@ -173,6 +176,8 @@ def receive_json(self) -> str:
# Limit to the first full JSON object
json_string = json_string[:end_index]
found = True
+ elif closed:
+ raise ConnectionError("Connection closed by the server")
if self.debug:
print("recv:", json_string)
From fb5bdefdaa88516691a006755293c8df5a222266 Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Wed, 30 Sep 2026 09:20:16 +0100
Subject: [PATCH 18/19] chore: remove typo'd __all_
---
.../sensors/filament_monitors/__init__.py | 16 ----------------
1 file changed, 16 deletions(-)
diff --git a/src/dsf/object_model/sensors/filament_monitors/__init__.py b/src/dsf/object_model/sensors/filament_monitors/__init__.py
index 301dfcd..1261a8c 100644
--- a/src/dsf/object_model/sensors/filament_monitors/__init__.py
+++ b/src/dsf/object_model/sensors/filament_monitors/__init__.py
@@ -14,22 +14,6 @@
RotatingMagnetFilamentMonitorConfigured,
)
-__all_ = [
- "Duet3DFilamentMonitor",
- "FilamentMonitor",
- "FilamentMonitorStatus",
- "FilamentMonitorType",
- "LaserFilamentMonitor",
- "LaserFilamentMonitorCalibrated",
- "LaserFilamentMonitorConfigured",
- "PulsedFilamentMonitor",
- "PulsedFilamentMonitorCalibrated",
- "PulsedFilamentMonitorConfigured",
- "RotatingMagnetFilamentMonitor",
- "RotatingMagnetFilamentMonitorCalibrated",
- "RotatingMagnetFilamentMonitorConfigured",
-]
-
__all__ = [
"Duet3DFilamentMonitor",
From 9f2cf373a6bc332c28ef88a45620da22bcc8164a Mon Sep 17 00:00:00 2001
From: AndyEveritt <38423143+AndyEveritt@users.noreply.github.com>
Date: Wed, 30 Sep 2026 09:22:25 +0100
Subject: [PATCH 19/19] refactor: remove blank line
---
src/dsf/object_model/sensors/filament_monitors/__init__.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/dsf/object_model/sensors/filament_monitors/__init__.py b/src/dsf/object_model/sensors/filament_monitors/__init__.py
index 1261a8c..5115c3a 100644
--- a/src/dsf/object_model/sensors/filament_monitors/__init__.py
+++ b/src/dsf/object_model/sensors/filament_monitors/__init__.py
@@ -14,7 +14,6 @@
RotatingMagnetFilamentMonitorConfigured,
)
-
__all__ = [
"Duet3DFilamentMonitor",
"FilamentMonitor",