mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-06-07 05:50:41 +02:00
feat: creating CallbackManager class which does all the callback related stuff
This commit is contained in:
parent
2e683df6ef
commit
4265929b4e
@ -37,7 +37,7 @@ def process_callable_attribute(attr: Any, args: dict[str, Any]) -> Any:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DataService(rpyc.Service, TaskManager):
|
class CallbackManager:
|
||||||
_list_mapping: dict[int, DataServiceList] = {}
|
_list_mapping: dict[int, DataServiceList] = {}
|
||||||
"""
|
"""
|
||||||
A dictionary mapping the id of the original lists to the corresponding
|
A dictionary mapping the id of the original lists to the corresponding
|
||||||
@ -47,135 +47,10 @@ class DataService(rpyc.Service, TaskManager):
|
|||||||
be tracked consistently. The keys of the dictionary are the ids of the original
|
be tracked consistently. The keys of the dictionary are the ids of the original
|
||||||
lists, and the values are the DataServiceList instances that wrap these lists.
|
lists, and the values are the DataServiceList instances that wrap these lists.
|
||||||
"""
|
"""
|
||||||
_notification_callbacks: list[Callable[[str, str, Any], Any]] = []
|
|
||||||
"""
|
|
||||||
A list of callback functions that are executed when a change occurs in the
|
|
||||||
DataService instance. These functions are intended to handle or respond to these
|
|
||||||
changes in some way, such as emitting a socket.io message to the frontend.
|
|
||||||
|
|
||||||
Each function in this list should be a callable that accepts three parameters:
|
def __init__(self, service: "DataService") -> None:
|
||||||
|
self.callbacks: set[Callable[[str, Any], None]] = set()
|
||||||
- parent_path (str): The path to the parent of the attribute that was changed.
|
self.service = service
|
||||||
- name (str): The name of the attribute that was changed.
|
|
||||||
- value (Any): The new value of the attribute.
|
|
||||||
|
|
||||||
A callback function can be added to this list using the add_notification_callback
|
|
||||||
method. Whenever a change in the DataService instance occurs (or in its nested
|
|
||||||
DataService or DataServiceList instances), the _emit_notification method is invoked,
|
|
||||||
which in turn calls all the callback functions in _notification_callbacks with the
|
|
||||||
appropriate arguments.
|
|
||||||
|
|
||||||
This implementation follows the observer pattern, with the DataService instance as
|
|
||||||
the "subject" and the callback functions as the "observers".
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, filename: Optional[str] = None) -> None:
|
|
||||||
TaskManager.__init__(self)
|
|
||||||
self.__root__: "DataService" = self
|
|
||||||
"""Keep track of the root object. This helps to filter the emission of
|
|
||||||
notifications. This overwrite the TaksManager's __root__ attribute."""
|
|
||||||
|
|
||||||
self._callbacks: set[Callable[[str, Any], None]] = set()
|
|
||||||
self._filename: Optional[str] = filename
|
|
||||||
|
|
||||||
self._register_callbacks()
|
|
||||||
self.__check_instance_classes()
|
|
||||||
self._initialised = True
|
|
||||||
self._load_values_from_json()
|
|
||||||
|
|
||||||
def _load_values_from_json(self) -> None:
|
|
||||||
if self._filename is not None:
|
|
||||||
# Check if the file specified by the filename exists
|
|
||||||
if os.path.exists(self._filename):
|
|
||||||
with open(self._filename, "r") as f:
|
|
||||||
# Load JSON data from file and update class attributes with these
|
|
||||||
# values
|
|
||||||
self.load_DataService_from_JSON(cast(dict[str, Any], json.load(f)))
|
|
||||||
|
|
||||||
def write_to_file(self) -> None:
|
|
||||||
"""
|
|
||||||
Serialize the DataService instance and write it to a JSON file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename (str): The name of the file to write to.
|
|
||||||
"""
|
|
||||||
if self._filename is not None:
|
|
||||||
with open(self._filename, "w") as f:
|
|
||||||
json.dump(self.serialize(), f, indent=4)
|
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
f"Class {self.__class__.__name__} was not initialised with a filename. "
|
|
||||||
'Skipping "write_to_file"...'
|
|
||||||
)
|
|
||||||
|
|
||||||
def load_DataService_from_JSON(self, json_dict: dict[str, Any]) -> None:
|
|
||||||
# Traverse the serialized representation and set the attributes of the class
|
|
||||||
serialized_class = self.serialize()
|
|
||||||
for path in generate_paths_from_DataService_dict(json_dict):
|
|
||||||
value = get_nested_value_by_path_and_key(json_dict, path=path)
|
|
||||||
value_type = get_nested_value_by_path_and_key(
|
|
||||||
json_dict, path=path, key="type"
|
|
||||||
)
|
|
||||||
class_value_type = get_nested_value_by_path_and_key(
|
|
||||||
serialized_class, path=path, key="type"
|
|
||||||
)
|
|
||||||
if class_value_type == value_type:
|
|
||||||
# Split the path into parts
|
|
||||||
parts = path.split(".")
|
|
||||||
attr_name = parts[-1]
|
|
||||||
|
|
||||||
self.update_DataService_attribute(parts[:-1], attr_name, value)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
f'Attribute type of "{path}" changed from "{value_type}" to '
|
|
||||||
f'"{class_value_type}". Ignoring value from JSON file...'
|
|
||||||
)
|
|
||||||
|
|
||||||
def __setattr__(self, __name: str, __value: Any) -> None:
|
|
||||||
current_value = getattr(self, __name, None)
|
|
||||||
# parse ints into floats if current value is a float
|
|
||||||
if isinstance(current_value, float) and isinstance(__value, int):
|
|
||||||
__value = float(__value)
|
|
||||||
|
|
||||||
super().__setattr__(__name, __value)
|
|
||||||
|
|
||||||
if self.__dict__.get("_initialised") and not __name == "_initialised":
|
|
||||||
for callback in self._callbacks:
|
|
||||||
callback(__name, __value)
|
|
||||||
elif __name.startswith(f"_{self.__class__.__name__}__"):
|
|
||||||
logger.warning(
|
|
||||||
f"Warning: You should not set private but rather protected attributes! "
|
|
||||||
f"Use {__name.replace(f'_{self.__class__.__name__}__', '_')} instead "
|
|
||||||
f"of {__name.replace(f'_{self.__class__.__name__}__', '__')}."
|
|
||||||
)
|
|
||||||
|
|
||||||
def _rpyc_getattr(self, name: str) -> Any:
|
|
||||||
if name.startswith("_"):
|
|
||||||
# disallow special and private attributes
|
|
||||||
raise AttributeError("cannot access private/special names")
|
|
||||||
# allow all other attributes
|
|
||||||
return getattr(self, name)
|
|
||||||
|
|
||||||
def _rpyc_setattr(self, name: str, value: Any) -> None:
|
|
||||||
if name.startswith("_"):
|
|
||||||
# disallow special and private attributes
|
|
||||||
raise AttributeError("cannot access private/special names")
|
|
||||||
|
|
||||||
# check if the attribute has a setter method
|
|
||||||
attr = getattr(self, name, None)
|
|
||||||
if isinstance(attr, property) and attr.fset is None:
|
|
||||||
raise AttributeError(f"{name} attribute does not have a setter method")
|
|
||||||
|
|
||||||
# allow all other attributes
|
|
||||||
setattr(self, name, value)
|
|
||||||
|
|
||||||
def _register_callbacks(self) -> None:
|
|
||||||
self._register_list_change_callbacks(self, f"{self.__class__.__name__}")
|
|
||||||
self._register_DataService_instance_callbacks(
|
|
||||||
self, f"{self.__class__.__name__}"
|
|
||||||
)
|
|
||||||
self._register_property_callbacks(self, f"{self.__class__.__name__}")
|
|
||||||
self._register_start_stop_task_callbacks(self, f"{self.__class__.__name__}")
|
|
||||||
|
|
||||||
def _register_list_change_callbacks( # noqa: C901
|
def _register_list_change_callbacks( # noqa: C901
|
||||||
self, obj: "DataService", parent_path: str
|
self, obj: "DataService", parent_path: str
|
||||||
@ -221,12 +96,12 @@ class DataService(rpyc.Service, TaskManager):
|
|||||||
# value at the time the lambda is defined, not when it is called. This
|
# value at the time the lambda is defined, not when it is called. This
|
||||||
# prevents attr_name from being overwritten in the next loop iteration.
|
# prevents attr_name from being overwritten in the next loop iteration.
|
||||||
callback = (
|
callback = (
|
||||||
lambda index, value, attr_name=attr_name: self._emit_notification(
|
lambda index, value, attr_name=attr_name: self.service._emit_notification(
|
||||||
parent_path=parent_path,
|
parent_path=parent_path,
|
||||||
name=f"{attr_name}[{index}]",
|
name=f"{attr_name}[{index}]",
|
||||||
value=value,
|
value=value,
|
||||||
)
|
)
|
||||||
if self == self.__root__
|
if self.service == self.service.__root__
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -285,7 +160,7 @@ class DataService(rpyc.Service, TaskManager):
|
|||||||
lambda name, value: obj._emit_notification(
|
lambda name, value: obj._emit_notification(
|
||||||
parent_path=parent_path, name=name, value=value
|
parent_path=parent_path, name=name, value=value
|
||||||
)
|
)
|
||||||
if self == obj.__root__
|
if self.service == obj.__root__
|
||||||
and not name.startswith("_") # we are only interested in public attributes
|
and not name.startswith("_") # we are only interested in public attributes
|
||||||
and not isinstance(
|
and not isinstance(
|
||||||
getattr(type(obj), name, None), property
|
getattr(type(obj), name, None), property
|
||||||
@ -293,7 +168,7 @@ class DataService(rpyc.Service, TaskManager):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
obj._callbacks.add(callback)
|
obj._callback_manager.callbacks.add(callback)
|
||||||
|
|
||||||
# Recursively register callbacks for all nested attributes of the object
|
# Recursively register callbacks for all nested attributes of the object
|
||||||
attrs = get_class_and_instance_attributes(obj)
|
attrs = get_class_and_instance_attributes(obj)
|
||||||
@ -325,7 +200,7 @@ class DataService(rpyc.Service, TaskManager):
|
|||||||
|
|
||||||
# as the DataService is an attribute of self, change the root object
|
# as the DataService is an attribute of self, change the root object
|
||||||
# use the dictionary to not trigger callbacks on initialised objects
|
# use the dictionary to not trigger callbacks on initialised objects
|
||||||
nested_attr.__dict__["__root__"] = self.__root__
|
nested_attr.__dict__["__root__"] = self.service.__root__
|
||||||
|
|
||||||
new_path = f"{parent_path}.{attr_name}"
|
new_path = f"{parent_path}.{attr_name}"
|
||||||
self._register_DataService_instance_callbacks(nested_attr, new_path)
|
self._register_DataService_instance_callbacks(nested_attr, new_path)
|
||||||
@ -356,7 +231,7 @@ class DataService(rpyc.Service, TaskManager):
|
|||||||
# changed, not reassigned)
|
# changed, not reassigned)
|
||||||
for item in obj_list:
|
for item in obj_list:
|
||||||
if isinstance(item, DataService):
|
if isinstance(item, DataService):
|
||||||
item._callbacks.add(callback)
|
item._callback_manager.callbacks.add(callback)
|
||||||
for attr_name in set(dir(item)) - set(dir(object)) - {"__root__"}:
|
for attr_name in set(dir(item)) - set(dir(object)) - {"__root__"}:
|
||||||
attr_value = getattr(item, attr_name)
|
attr_value = getattr(item, attr_name)
|
||||||
if isinstance(attr_value, (DataService, DataServiceList)):
|
if isinstance(attr_value, (DataService, DataServiceList)):
|
||||||
@ -421,7 +296,7 @@ class DataService(rpyc.Service, TaskManager):
|
|||||||
name=dependent_attr,
|
name=dependent_attr,
|
||||||
value=getattr(obj, dependent_attr),
|
value=getattr(obj, dependent_attr),
|
||||||
)
|
)
|
||||||
if self == obj.__root__
|
if self.service == obj.__root__
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -436,11 +311,188 @@ class DataService(rpyc.Service, TaskManager):
|
|||||||
name=dependent_attr,
|
name=dependent_attr,
|
||||||
value=getattr(obj, dependent_attr),
|
value=getattr(obj, dependent_attr),
|
||||||
)
|
)
|
||||||
if name == dep and self == obj.__root__
|
if name == dep and self.service == obj.__root__
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
# Add to _callbacks
|
# Add to _callbacks
|
||||||
obj._callbacks.add(callback)
|
obj._callback_manager.callbacks.add(callback)
|
||||||
|
|
||||||
|
def _register_start_stop_task_callbacks(
|
||||||
|
self, obj: "DataService", parent_path: str
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
This function registers callbacks for start and stop methods of async functions.
|
||||||
|
These callbacks are stored in the '_task_status_change_callbacks' attribute and
|
||||||
|
are called when the status of a task changes.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
obj: DataService
|
||||||
|
The target object on which callbacks are to be registered.
|
||||||
|
parent_path: str
|
||||||
|
The access path for the parent object. This is used to construct the full
|
||||||
|
access path for the notifications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create and register a callback for the object
|
||||||
|
# only emit the notification when the call was registered by the root object
|
||||||
|
callback: Callable[[str, dict[str, Any] | None], None] = (
|
||||||
|
lambda name, status: obj._emit_notification(
|
||||||
|
parent_path=parent_path, name=name, value=status
|
||||||
|
)
|
||||||
|
if self.service == obj.__root__
|
||||||
|
and not name.startswith("_") # we are only interested in public attributes
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
obj._task_status_change_callbacks.append(callback)
|
||||||
|
|
||||||
|
# Recursively register callbacks for all nested attributes of the object
|
||||||
|
attrs: dict[str, Any] = get_class_and_instance_attributes(obj)
|
||||||
|
|
||||||
|
for nested_attr_name, nested_attr in attrs.items():
|
||||||
|
if isinstance(nested_attr, DataService):
|
||||||
|
self._register_start_stop_task_callbacks(
|
||||||
|
nested_attr, parent_path=f"{parent_path}.{nested_attr_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def register_callbacks(self) -> None:
|
||||||
|
self._register_list_change_callbacks(
|
||||||
|
self.service, f"{self.service.__class__.__name__}"
|
||||||
|
)
|
||||||
|
self._register_DataService_instance_callbacks(
|
||||||
|
self.service, f"{self.service.__class__.__name__}"
|
||||||
|
)
|
||||||
|
self._register_property_callbacks(
|
||||||
|
self.service, f"{self.service.__class__.__name__}"
|
||||||
|
)
|
||||||
|
self._register_start_stop_task_callbacks(
|
||||||
|
self.service, f"{self.service.__class__.__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataService(rpyc.Service, TaskManager):
|
||||||
|
_notification_callbacks: list[Callable[[str, str, Any], Any]] = []
|
||||||
|
"""
|
||||||
|
A list of callback functions that are executed when a change occurs in the
|
||||||
|
DataService instance. These functions are intended to handle or respond to these
|
||||||
|
changes in some way, such as emitting a socket.io message to the frontend.
|
||||||
|
|
||||||
|
Each function in this list should be a callable that accepts three parameters:
|
||||||
|
|
||||||
|
- parent_path (str): The path to the parent of the attribute that was changed.
|
||||||
|
- name (str): The name of the attribute that was changed.
|
||||||
|
- value (Any): The new value of the attribute.
|
||||||
|
|
||||||
|
A callback function can be added to this list using the add_notification_callback
|
||||||
|
method. Whenever a change in the DataService instance occurs (or in its nested
|
||||||
|
DataService or DataServiceList instances), the _emit_notification method is invoked,
|
||||||
|
which in turn calls all the callback functions in _notification_callbacks with the
|
||||||
|
appropriate arguments.
|
||||||
|
|
||||||
|
This implementation follows the observer pattern, with the DataService instance as
|
||||||
|
the "subject" and the callback functions as the "observers".
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, filename: Optional[str] = None) -> None:
|
||||||
|
TaskManager.__init__(self)
|
||||||
|
self._callback_manager = CallbackManager(self)
|
||||||
|
self.__root__: "DataService" = self
|
||||||
|
"""Keep track of the root object. This helps to filter the emission of
|
||||||
|
notifications. This overwrite the TaksManager's __root__ attribute."""
|
||||||
|
|
||||||
|
self._filename: Optional[str] = filename
|
||||||
|
|
||||||
|
self._callback_manager.register_callbacks()
|
||||||
|
self.__check_instance_classes()
|
||||||
|
self._initialised = True
|
||||||
|
self._load_values_from_json()
|
||||||
|
|
||||||
|
def _load_values_from_json(self) -> None:
|
||||||
|
if self._filename is not None:
|
||||||
|
# Check if the file specified by the filename exists
|
||||||
|
if os.path.exists(self._filename):
|
||||||
|
with open(self._filename, "r") as f:
|
||||||
|
# Load JSON data from file and update class attributes with these
|
||||||
|
# values
|
||||||
|
self.load_DataService_from_JSON(cast(dict[str, Any], json.load(f)))
|
||||||
|
|
||||||
|
def write_to_file(self) -> None:
|
||||||
|
"""
|
||||||
|
Serialize the DataService instance and write it to a JSON file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename (str): The name of the file to write to.
|
||||||
|
"""
|
||||||
|
if self._filename is not None:
|
||||||
|
with open(self._filename, "w") as f:
|
||||||
|
json.dump(self.serialize(), f, indent=4)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Class {self.__class__.__name__} was not initialised with a filename. "
|
||||||
|
'Skipping "write_to_file"...'
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_DataService_from_JSON(self, json_dict: dict[str, Any]) -> None:
|
||||||
|
# Traverse the serialized representation and set the attributes of the class
|
||||||
|
serialized_class = self.serialize()
|
||||||
|
for path in generate_paths_from_DataService_dict(json_dict):
|
||||||
|
value = get_nested_value_by_path_and_key(json_dict, path=path)
|
||||||
|
value_type = get_nested_value_by_path_and_key(
|
||||||
|
json_dict, path=path, key="type"
|
||||||
|
)
|
||||||
|
class_value_type = get_nested_value_by_path_and_key(
|
||||||
|
serialized_class, path=path, key="type"
|
||||||
|
)
|
||||||
|
if class_value_type == value_type:
|
||||||
|
# Split the path into parts
|
||||||
|
parts = path.split(".")
|
||||||
|
attr_name = parts[-1]
|
||||||
|
|
||||||
|
self.update_DataService_attribute(parts[:-1], attr_name, value)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f'Attribute type of "{path}" changed from "{value_type}" to '
|
||||||
|
f'"{class_value_type}". Ignoring value from JSON file...'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __setattr__(self, __name: str, __value: Any) -> None:
|
||||||
|
current_value = getattr(self, __name, None)
|
||||||
|
# parse ints into floats if current value is a float
|
||||||
|
if isinstance(current_value, float) and isinstance(__value, int):
|
||||||
|
__value = float(__value)
|
||||||
|
|
||||||
|
super().__setattr__(__name, __value)
|
||||||
|
|
||||||
|
if self.__dict__.get("_initialised") and not __name == "_initialised":
|
||||||
|
for callback in self._callback_manager.callbacks:
|
||||||
|
callback(__name, __value)
|
||||||
|
elif __name.startswith(f"_{self.__class__.__name__}__"):
|
||||||
|
logger.warning(
|
||||||
|
f"Warning: You should not set private but rather protected attributes! "
|
||||||
|
f"Use {__name.replace(f'_{self.__class__.__name__}__', '_')} instead "
|
||||||
|
f"of {__name.replace(f'_{self.__class__.__name__}__', '__')}."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _rpyc_getattr(self, name: str) -> Any:
|
||||||
|
if name.startswith("_"):
|
||||||
|
# disallow special and private attributes
|
||||||
|
raise AttributeError("cannot access private/special names")
|
||||||
|
# allow all other attributes
|
||||||
|
return getattr(self, name)
|
||||||
|
|
||||||
|
def _rpyc_setattr(self, name: str, value: Any) -> None:
|
||||||
|
if name.startswith("_"):
|
||||||
|
# disallow special and private attributes
|
||||||
|
raise AttributeError("cannot access private/special names")
|
||||||
|
|
||||||
|
# check if the attribute has a setter method
|
||||||
|
attr = getattr(self, name, None)
|
||||||
|
if isinstance(attr, property) and attr.fset is None:
|
||||||
|
raise AttributeError(f"{name} attribute does not have a setter method")
|
||||||
|
|
||||||
|
# allow all other attributes
|
||||||
|
setattr(self, name, value)
|
||||||
|
|
||||||
def __check_instance_classes(self) -> None:
|
def __check_instance_classes(self) -> None:
|
||||||
for attr_name, attr_value in get_class_and_instance_attributes(self).items():
|
for attr_name, attr_value in get_class_and_instance_attributes(self).items():
|
||||||
|
@ -95,45 +95,6 @@ class TaskManager(ABC):
|
|||||||
|
|
||||||
self._set_start_and_stop_for_async_methods()
|
self._set_start_and_stop_for_async_methods()
|
||||||
|
|
||||||
def _register_start_stop_task_callbacks(
|
|
||||||
self, obj: "TaskManager", parent_path: str
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
This function registers callbacks for start and stop methods of async functions.
|
|
||||||
These callbacks are stored in the '_task_status_change_callbacks' attribute and
|
|
||||||
are called when the status of a task changes.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
-----------
|
|
||||||
obj: DataService
|
|
||||||
The target object on which callbacks are to be registered.
|
|
||||||
parent_path: str
|
|
||||||
The access path for the parent object. This is used to construct the full
|
|
||||||
access path for the notifications.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Create and register a callback for the object
|
|
||||||
# only emit the notification when the call was registered by the root object
|
|
||||||
callback: Callable[[str, dict[str, Any] | None], None] = (
|
|
||||||
lambda name, status: obj._emit_notification(
|
|
||||||
parent_path=parent_path, name=name, value=status
|
|
||||||
)
|
|
||||||
if self == obj.__root__
|
|
||||||
and not name.startswith("_") # we are only interested in public attributes
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
obj._task_status_change_callbacks.append(callback)
|
|
||||||
|
|
||||||
# Recursively register callbacks for all nested attributes of the object
|
|
||||||
attrs: dict[str, Any] = get_class_and_instance_attributes(obj)
|
|
||||||
|
|
||||||
for nested_attr_name, nested_attr in attrs.items():
|
|
||||||
if isinstance(nested_attr, TaskManager):
|
|
||||||
self._register_start_stop_task_callbacks(
|
|
||||||
nested_attr, parent_path=f"{parent_path}.{nested_attr_name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _set_start_and_stop_for_async_methods(self) -> None: # noqa: C901
|
def _set_start_and_stop_for_async_methods(self) -> None: # noqa: C901
|
||||||
# inspect the methods of the class
|
# inspect the methods of the class
|
||||||
for name, method in inspect.getmembers(
|
for name, method in inspect.getmembers(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user