mirror of
https://github.com/ZGCA-Forge/MsgCenterPy.git
synced 2026-02-04 13:25:19 +00:00
init version
This commit is contained in:
0
msgcenterpy/core/__init__.py
Normal file
0
msgcenterpy/core/__init__.py
Normal file
54
msgcenterpy/core/envelope.py
Normal file
54
msgcenterpy/core/envelope.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, TypedDict
|
||||
|
||||
ENVELOPE_VERSION: str = "1"
|
||||
|
||||
|
||||
class Properties(TypedDict, total=False):
|
||||
ros_msg_cls_path: str
|
||||
ros_msg_cls_namespace: str
|
||||
json_schema: Dict[str, Any]
|
||||
|
||||
|
||||
class FormatMetadata(TypedDict, total=False):
|
||||
"""Additional metadata for source format, optional.
|
||||
|
||||
Examples: field statistics, original type descriptions, field type mappings, etc.
|
||||
"""
|
||||
|
||||
current_format: str
|
||||
source_cls_name: str
|
||||
source_cls_module: str
|
||||
properties: Properties
|
||||
|
||||
|
||||
class MessageEnvelope(TypedDict, total=True):
|
||||
"""Unified message envelope format.
|
||||
|
||||
- version: Protocol version
|
||||
- format: Source format (MessageType.value)
|
||||
- type_info: Type information (applicable for ROS2, Pydantic, etc.)
|
||||
- content: Normalized message content (dictionary)
|
||||
- metadata: Additional metadata
|
||||
"""
|
||||
|
||||
version: str
|
||||
format: str
|
||||
content: Dict[str, Any]
|
||||
metadata: FormatMetadata
|
||||
|
||||
|
||||
def create_envelope(
|
||||
*,
|
||||
format_name: str,
|
||||
content: Dict[str, Any],
|
||||
metadata: FormatMetadata,
|
||||
) -> MessageEnvelope:
|
||||
env: MessageEnvelope = {
|
||||
"version": ENVELOPE_VERSION,
|
||||
"format": format_name,
|
||||
"content": content,
|
||||
"metadata": metadata,
|
||||
}
|
||||
return env
|
||||
406
msgcenterpy/core/field_accessor.py
Normal file
406
msgcenterpy/core/field_accessor.py
Normal file
@@ -0,0 +1,406 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
from msgcenterpy.core.type_converter import StandardType
|
||||
from msgcenterpy.core.type_info import (
|
||||
ConstraintType,
|
||||
Consts,
|
||||
TypeInfo,
|
||||
TypeInfoPostProcessor,
|
||||
)
|
||||
from msgcenterpy.utils.decorator import experimental
|
||||
|
||||
TEST_MODE = True
|
||||
|
||||
|
||||
class FieldAccessor:
|
||||
"""
|
||||
字段访问器,支持类型转换和约束验证的统一字段访问接口
|
||||
只需要getitem和setitem,外部必须通过字典的方式来赋值
|
||||
"""
|
||||
|
||||
@property
|
||||
def parent_msg_center(self) -> Optional["FieldAccessor"]:
|
||||
return self._parent
|
||||
|
||||
@property
|
||||
def full_path_from_root(self) -> str:
|
||||
if self._parent is None:
|
||||
return self._field_name or "unknown"
|
||||
else:
|
||||
parent_path = self._parent.full_path_from_root
|
||||
return f"{parent_path}.{self._field_name or 'unknown'}"
|
||||
|
||||
@property
|
||||
def root_accessor_msg_center(self) -> "FieldAccessor":
|
||||
"""获取根访问器"""
|
||||
current = self
|
||||
while current._parent is not None:
|
||||
current = current._parent
|
||||
return current
|
||||
|
||||
@property
|
||||
def value(self) -> Any:
|
||||
return self._data
|
||||
|
||||
@value.setter
|
||||
def value(self, data: Any) -> None:
|
||||
if self._parent is not None and self._field_name is not None:
|
||||
self._parent[self._field_name] = data
|
||||
|
||||
@property
|
||||
def type_info(self) -> Optional[TypeInfo]:
|
||||
if self._type_info is not None:
|
||||
return self._type_info
|
||||
|
||||
# 如果是根accessor或者没有字段名,无法获取TypeInfo
|
||||
if self._parent is None or self._field_name is None:
|
||||
return None
|
||||
|
||||
# 调用类型信息提供者获取类型信息,调用是耗时的
|
||||
if self._type_info_provider is None:
|
||||
return None
|
||||
type_info = self._type_info_provider.get_field_type_info(self._field_name, self._data, self._parent)
|
||||
|
||||
# 对TypeInfo进行后处理,添加默认约束
|
||||
if type_info:
|
||||
TypeInfoPostProcessor.post_process_type_info(type_info)
|
||||
self._type_info = type_info
|
||||
|
||||
return type_info
|
||||
|
||||
"""标记方便排除getitem/setitem,不要删除"""
|
||||
_data: Any = None
|
||||
_type_info_provider: "TypeInfoProvider" = None # type: ignore[assignment]
|
||||
_parent: Optional["FieldAccessor"] = None
|
||||
_field_name: str = None # type: ignore[assignment]
|
||||
_cache: Dict[str, "FieldAccessor"] = None # type: ignore[assignment]
|
||||
_type_info: Optional[TypeInfo] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: Any,
|
||||
type_info_provider: "TypeInfoProvider",
|
||||
parent: Optional["FieldAccessor"],
|
||||
field_name: str,
|
||||
):
|
||||
"""
|
||||
初始化字段访问器
|
||||
|
||||
Args:
|
||||
data: 要访问的数据对象
|
||||
type_info_provider: 类型信息提供者
|
||||
parent: 父字段访问器,用于嵌套访问
|
||||
field_name: 当前访问器对应的字段名(用于构建路径)
|
||||
"""
|
||||
self._data = data
|
||||
self._type_info_provider = type_info_provider
|
||||
self._parent = parent
|
||||
self._field_name = field_name
|
||||
self._cache: Dict[str, "FieldAccessor"] = {} # 缓存FieldAccessor而不是TypeInfo
|
||||
self._type_info: Optional[TypeInfo] = None # 当前accessor的TypeInfo
|
||||
|
||||
def get_sub_type_info(self, field_name: str) -> Optional[TypeInfo]:
|
||||
"""获取字段的类型信息,通过获取字段的accessor"""
|
||||
field_accessor = self[field_name]
|
||||
return field_accessor.type_info
|
||||
|
||||
def __getitem__(self, field_name: str) -> "FieldAccessor":
|
||||
"""获取字段访问器,支持嵌套访问"""
|
||||
# 检查缓存中是否有对应的 accessor
|
||||
if self._cache is None:
|
||||
self._cache = {}
|
||||
if field_name in self._cache:
|
||||
cached_accessor = self._cache[field_name]
|
||||
# 更新 accessor 的数据源,以防数据已更改
|
||||
if TEST_MODE:
|
||||
raw_value = self._get_raw_value(field_name)
|
||||
if cached_accessor.value != raw_value:
|
||||
raise ValueError(
|
||||
f"Cached accessor value mismatch for field '{field_name}': expected {raw_value}, got {cached_accessor.value}"
|
||||
)
|
||||
return cached_accessor
|
||||
# 获取原始值并创建新的 accessor
|
||||
raw_value = self._get_raw_value(field_name)
|
||||
if self._type_info_provider is None:
|
||||
raise RuntimeError("TypeInfoProvider not initialized")
|
||||
accessor = FieldAccessorFactory.create_accessor(
|
||||
data=raw_value,
|
||||
type_info_provider=self._type_info_provider,
|
||||
parent=self,
|
||||
field_name=field_name,
|
||||
)
|
||||
|
||||
self._cache[field_name] = accessor
|
||||
return accessor
|
||||
|
||||
def __setitem__(self, field_name: str, value: Any) -> None:
|
||||
"""设置字段值,支持类型转换和验证"""
|
||||
# 获取类型信息
|
||||
if field_name in self._get_field_names():
|
||||
type_info = self.get_sub_type_info(field_name)
|
||||
if type_info is not None:
|
||||
# 进行类型转换
|
||||
converted_value = type_info.convert_value(value) # 这步自带validate
|
||||
value = converted_value
|
||||
# 对子field设置value,依然会上溯走set_raw_value,确保一致性
|
||||
# 设置值
|
||||
sub_accessor = self[field_name]
|
||||
self._set_raw_value(field_name, value)
|
||||
sub_accessor._data = self._get_raw_value(field_name) # 有可能内部还有赋值的处理
|
||||
# 清除相关缓存
|
||||
self.clear_cache(field_name)
|
||||
|
||||
def __contains__(self, field_name: str) -> bool:
|
||||
return self._has_field(field_name)
|
||||
|
||||
def __getattr__(self, field_name: str) -> "FieldAccessor | Any":
|
||||
"""支持通过属性访问字段,用于嵌套访问如 accessor.pose.position.x"""
|
||||
for cls in self.__class__.__mro__:
|
||||
if field_name in cls.__dict__:
|
||||
return cast(Any, super().__getattribute__(field_name))
|
||||
return self[field_name]
|
||||
|
||||
def __setattr__(self, field_name: str, value: Any) -> None:
|
||||
"""支持通过属性设置字段值,用于嵌套赋值如 accessor.pose.position.x = 1.0"""
|
||||
for cls in self.__class__.__mro__:
|
||||
if field_name in cls.__dict__:
|
||||
return super().__setattr__(field_name, value)
|
||||
self[field_name] = value
|
||||
return None
|
||||
|
||||
def clear_cache(self, field_name: Optional[str] = None) -> None:
|
||||
"""失效字段相关的缓存"""
|
||||
if self._cache is not None and field_name is not None and field_name in self._cache:
|
||||
self._cache[field_name].clear_type_info()
|
||||
|
||||
def clear_type_info(self) -> None:
|
||||
"""清空所有缓存"""
|
||||
if self._type_info is not None:
|
||||
self._type_info._outdated = True
|
||||
self._type_info = None
|
||||
|
||||
def get_nested_field_accessor(self, path: str, separator: str = ".") -> "FieldAccessor":
|
||||
parts = path.split(separator)
|
||||
current = self
|
||||
for part in parts:
|
||||
current = self[part]
|
||||
return current
|
||||
|
||||
def set_nested_value(self, path: str, value: Any, separator: str = ".") -> None:
|
||||
current = self.get_nested_field_accessor(path, separator)
|
||||
current.value = value
|
||||
|
||||
def _get_raw_value(self, field_name: str) -> Any:
|
||||
"""获取原始字段值(子类实现)"""
|
||||
if hasattr(self._data, "__getitem__"):
|
||||
return self._data[field_name]
|
||||
elif hasattr(self._data, field_name):
|
||||
return getattr(self._data, field_name)
|
||||
else:
|
||||
raise KeyError(f"Field {field_name} not found")
|
||||
|
||||
def _set_raw_value(self, field_name: str, value: Any) -> None:
|
||||
"""设置原始字段值(子类实现)"""
|
||||
if hasattr(self._data, "__setitem__"):
|
||||
self._data[field_name] = value
|
||||
elif hasattr(self._data, field_name):
|
||||
setattr(self._data, field_name, value)
|
||||
else:
|
||||
raise KeyError(f"Field {field_name} not found")
|
||||
|
||||
def _has_field(self, field_name: str) -> bool:
|
||||
"""检查字段是否存在(子类实现)"""
|
||||
if hasattr(self._data, "__contains__"):
|
||||
return field_name in self._data
|
||||
else:
|
||||
return field_name in self._get_field_names()
|
||||
|
||||
def _get_field_names(self) -> list[str]:
|
||||
"""获取所有字段名(子类实现)"""
|
||||
if callable(getattr(self._data, "keys", None)):
|
||||
# noinspection PyCallingNonCallable
|
||||
return list(self._data.keys())
|
||||
elif hasattr(self._data, "__dict__"):
|
||||
return list(self._data.__dict__.keys())
|
||||
elif hasattr(self._data, "__slots__"):
|
||||
# noinspection PyTypeChecker
|
||||
return list(self._data.__slots__)
|
||||
else:
|
||||
# 回退方案:使用dir()但过滤掉特殊方法
|
||||
return [name for name in dir(self._data) if not name.startswith("_")]
|
||||
|
||||
def get_json_schema(self) -> Dict[str, Any]:
|
||||
"""原有的递归生成 JSON Schema 逻辑"""
|
||||
# 获取当前访问器的类型信息
|
||||
current_type_info = self.type_info
|
||||
|
||||
# 如果当前层级有类型信息,使用它生成基本schema
|
||||
if current_type_info is not None:
|
||||
schema = current_type_info.to_json_schema_property()
|
||||
else:
|
||||
# 如果没有类型信息,创建基本的object schema
|
||||
schema = {"type": "object", "additionalProperties": True}
|
||||
|
||||
# 如果这是一个对象类型,需要递归处理其字段
|
||||
if schema.get("type") == "object":
|
||||
properties = {}
|
||||
required_fields = []
|
||||
|
||||
# 获取所有字段名
|
||||
field_names = self._get_field_names()
|
||||
|
||||
for field_name in field_names:
|
||||
try:
|
||||
# 获取字段的访问器
|
||||
field_accessor = self[field_name]
|
||||
field_type_info = field_accessor.type_info
|
||||
|
||||
if field_type_info is not None:
|
||||
# 根据字段类型决定如何生成schema
|
||||
if field_type_info.standard_type == StandardType.OBJECT:
|
||||
# 对于嵌套对象,递归调用
|
||||
field_schema = field_accessor.get_json_schema()
|
||||
else:
|
||||
# 对于基本类型,直接使用type_info转换
|
||||
field_schema = field_type_info.to_json_schema_property()
|
||||
|
||||
properties[field_name] = field_schema
|
||||
|
||||
# 检查是否为必需字段
|
||||
if field_type_info.has_constraint(ConstraintType.REQUIRED):
|
||||
required_fields.append(field_name)
|
||||
|
||||
except Exception as e:
|
||||
# 如果字段处理失败,记录警告但继续处理其他字段
|
||||
print(f"Warning: Failed to generate schema for field '{field_name}': {e}")
|
||||
continue
|
||||
|
||||
# 更新schema中的properties
|
||||
if properties:
|
||||
schema["properties"] = properties
|
||||
|
||||
# 设置必需字段
|
||||
if required_fields:
|
||||
schema["required"] = required_fields
|
||||
|
||||
# 如果没有字段信息,允许额外属性
|
||||
if not properties:
|
||||
schema["additionalProperties"] = True
|
||||
else:
|
||||
schema["additionalProperties"] = False
|
||||
|
||||
return schema
|
||||
|
||||
@experimental("Feature under development")
|
||||
def update_from_dict(self, source_data: Dict[str, Any]) -> None:
|
||||
"""递归更新嵌套字典
|
||||
|
||||
Args:
|
||||
source_data: 源数据字典
|
||||
"""
|
||||
for key, new_value in source_data.items():
|
||||
field_exists = self._has_field(key)
|
||||
could_add = self._could_allow_new_field(key, new_value)
|
||||
if field_exists:
|
||||
current_field_accessor = self[key]
|
||||
current_type_info = current_field_accessor.type_info
|
||||
# 当前key: Object,交给子dict去迭代
|
||||
if (
|
||||
current_type_info
|
||||
and current_type_info.standard_type == StandardType.OBJECT
|
||||
and isinstance(new_value, dict)
|
||||
):
|
||||
current_field_accessor.update_from_dict(new_value)
|
||||
# 当前key: Array,每个值要交给子array去迭代
|
||||
elif (
|
||||
current_type_info
|
||||
and hasattr(current_type_info.standard_type, "IS_ARRAY")
|
||||
and current_type_info.standard_type.IS_ARRAY
|
||||
and isinstance(new_value, list)
|
||||
):
|
||||
# 存在情况Array嵌套,这里后续支持逐个赋值,可能需要利用iter进行赋值
|
||||
self[key] = new_value
|
||||
else:
|
||||
# 不限制类型 或 python类型包含
|
||||
if could_add or (current_type_info and issubclass(type(new_value), current_type_info.python_type)):
|
||||
self[key] = new_value
|
||||
else:
|
||||
raise ValueError(f"{key}")
|
||||
elif could_add:
|
||||
self[key] = new_value
|
||||
|
||||
def _could_allow_new_field(self, field_name: str, field_value: Any) -> bool:
|
||||
"""检查是否应该允许添加新字段
|
||||
|
||||
通过检查当前type_info中的TYPE_KEEP约束来判断:
|
||||
- 如果有TYPE_KEEP且为True,说明类型结构固定,不允许添加新字段
|
||||
- 如果没有TYPE_KEEP约束或为False,则允许添加新字段
|
||||
|
||||
Args:
|
||||
field_name: 字段名
|
||||
field_value: 字段值
|
||||
|
||||
Returns:
|
||||
是否允许添加该字段
|
||||
"""
|
||||
parent_type_info = self.type_info
|
||||
if parent_type_info is None:
|
||||
return True # DEBUGGER NEEDED
|
||||
type_keep_constraint = parent_type_info.get_constraint(ConstraintType.TYPE_KEEP)
|
||||
if type_keep_constraint is not None and type_keep_constraint.value:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class TypeInfoProvider(ABC):
|
||||
"""Require All Message Instances Extends This get_field_typ_info"""
|
||||
|
||||
@abstractmethod
|
||||
def get_field_type_info(
|
||||
self, field_name: str, field_value: Any, field_accessor: "FieldAccessor"
|
||||
) -> Optional[TypeInfo]:
|
||||
"""获取指定字段的类型信息
|
||||
|
||||
Args:
|
||||
field_name: 字段名,简单字段名如 'field'
|
||||
field_value: 字段的当前值,用于动态类型推断,不能为None
|
||||
field_accessor: 字段访问器,提供额外的上下文信息,不能为None
|
||||
|
||||
Returns:
|
||||
字段的TypeInfo,如果字段不存在则返回None
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ROS2FieldAccessor(FieldAccessor):
|
||||
def _get_raw_value(self, field_name: str) -> Any:
|
||||
return getattr(self._data, field_name)
|
||||
|
||||
def _set_raw_value(self, field_name: str, value: Any) -> None:
|
||||
return setattr(self._data, field_name, value)
|
||||
|
||||
def _has_field(self, field_name: str) -> bool:
|
||||
return hasattr(self._data, field_name)
|
||||
|
||||
def _get_field_names(self) -> list[str]:
|
||||
if hasattr(self._data, "_fields_and_field_types"):
|
||||
# noinspection PyProtectedMember
|
||||
fields_and_types: Dict[str, str] = cast(Dict[str, str], self._data._fields_and_field_types)
|
||||
return list(fields_and_types.keys())
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class FieldAccessorFactory:
|
||||
@staticmethod
|
||||
def create_accessor(
|
||||
data: Any,
|
||||
type_info_provider: TypeInfoProvider,
|
||||
parent: Optional[FieldAccessor] = None,
|
||||
field_name: str = Consts.ACCESSOR_ROOT_NODE,
|
||||
) -> FieldAccessor:
|
||||
if hasattr(data, "_fields_and_field_types"):
|
||||
return ROS2FieldAccessor(data, type_info_provider, parent, field_name)
|
||||
else:
|
||||
return FieldAccessor(data, type_info_provider, parent, field_name)
|
||||
69
msgcenterpy/core/message_center.py
Normal file
69
msgcenterpy/core/message_center.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
from msgcenterpy.core.envelope import MessageEnvelope, Properties
|
||||
from msgcenterpy.core.message_instance import MessageInstance
|
||||
from msgcenterpy.core.types import MessageType
|
||||
|
||||
|
||||
class MessageCenter:
|
||||
"""Message Center singleton class that manages all message types and instances"""
|
||||
|
||||
_instance: Optional["MessageCenter"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "MessageCenter":
|
||||
"""Get MessageCenter singleton instance"""
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Private constructor, use get_instance() to get singleton"""
|
||||
self._type_registry: Dict[MessageType, Type[MessageInstance]] = {}
|
||||
self._register_builtin_types()
|
||||
|
||||
def _register_builtin_types(self) -> None:
|
||||
"""Register built-in message types with lazy import to avoid circular dependencies"""
|
||||
try:
|
||||
from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
|
||||
|
||||
self._type_registry[MessageType.ROS2] = ROS2MessageInstance
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from msgcenterpy.instances.json_schema_instance import (
|
||||
JSONSchemaMessageInstance,
|
||||
)
|
||||
|
||||
self._type_registry[MessageType.JSON_SCHEMA] = JSONSchemaMessageInstance
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def get_instance_class(self, message_type: MessageType) -> Type[MessageInstance]:
|
||||
"""Get instance class for the specified message type"""
|
||||
instance_class = self._type_registry.get(message_type)
|
||||
if not instance_class:
|
||||
raise ValueError(f"Unsupported message type: {message_type}")
|
||||
return instance_class
|
||||
|
||||
def convert(
|
||||
self,
|
||||
source: MessageInstance,
|
||||
target_type: MessageType,
|
||||
override_properties: Dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> MessageInstance:
|
||||
"""Convert message types"""
|
||||
target_class = self.get_instance_class(target_type)
|
||||
dict_data: MessageEnvelope = source.export_to_envelope()
|
||||
if "properties" not in dict_data["metadata"]:
|
||||
dict_data["metadata"]["properties"] = Properties()
|
||||
dict_data["metadata"]["properties"].update(override_properties) # type: ignore[typeddict-item]
|
||||
target_instance = target_class.import_from_envelope(dict_data)
|
||||
return target_instance
|
||||
|
||||
|
||||
# Module-level convenience function using singleton
|
||||
def get_message_center() -> MessageCenter:
|
||||
"""Get message center singleton"""
|
||||
return MessageCenter.get_instance()
|
||||
193
msgcenterpy/core/message_instance.py
Normal file
193
msgcenterpy/core/message_instance.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar, cast
|
||||
|
||||
from msgcenterpy.core.envelope import FormatMetadata, MessageEnvelope, Properties
|
||||
from msgcenterpy.core.field_accessor import (
|
||||
FieldAccessor,
|
||||
FieldAccessorFactory,
|
||||
TypeInfoProvider,
|
||||
)
|
||||
from msgcenterpy.core.types import MessageType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# 仅用于类型检查的导入,避免运行时循环依赖
|
||||
from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
|
||||
from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class MessageInstance(TypeInfoProvider, ABC, Generic[T]):
|
||||
"""统一消息实例基类"""
|
||||
|
||||
_init_ok: bool = False
|
||||
|
||||
# 字段访问器相关方法
|
||||
@property
|
||||
def fields(self) -> FieldAccessor:
|
||||
if self._field_accessor is None:
|
||||
raise RuntimeError("FieldAccessor not initialized")
|
||||
return self._field_accessor
|
||||
|
||||
def __setattr__(self, field_name: str, value: Any) -> None:
|
||||
if not self._init_ok:
|
||||
return super().__setattr__(field_name, value)
|
||||
for cls in self.__class__.__mro__:
|
||||
if field_name in cls.__dict__:
|
||||
return super().__setattr__(field_name, value)
|
||||
self.fields[field_name] = value
|
||||
return None
|
||||
|
||||
def __getattr__(self, field_name: str) -> Any:
|
||||
if not self._init_ok:
|
||||
return super().__getattribute__(field_name)
|
||||
for cls in self.__class__.__mro__:
|
||||
if field_name in cls.__dict__:
|
||||
return super().__getattribute__(field_name)
|
||||
return self.fields[field_name]
|
||||
|
||||
def __getitem__(self, field_name: str) -> Any:
|
||||
"""支持通过下标访问字段"""
|
||||
return self.fields[field_name]
|
||||
|
||||
def __setitem__(self, field_name: str, value: Any) -> None:
|
||||
"""支持通过下标设置字段"""
|
||||
self.fields[field_name] = value
|
||||
|
||||
def __contains__(self, field_name: str) -> bool:
|
||||
"""支持in操作符检查字段是否存在"""
|
||||
return field_name in self.fields
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
inner_data: T,
|
||||
message_type: MessageType,
|
||||
metadata: Optional[FormatMetadata] = None,
|
||||
):
|
||||
# 初始化标记和基础属性
|
||||
self._field_accessor: Optional[FieldAccessor] = None
|
||||
|
||||
self._instance_id: str = str(uuid.uuid4())
|
||||
self.inner_data: T = inner_data # 原始类型数据
|
||||
self.message_type: MessageType = message_type
|
||||
self._metadata: FormatMetadata = metadata or FormatMetadata()
|
||||
self._created_at = datetime.now(timezone.utc)
|
||||
self._collect_public_properties_to_metadata()
|
||||
self._field_accessor = FieldAccessorFactory.create_accessor(self.inner_data, self)
|
||||
self._init_ok = True
|
||||
|
||||
def _collect_public_properties_to_metadata(self) -> None:
|
||||
"""将所有非下划线开头的 @property 的当前值放入 metadata.properties 中。
|
||||
|
||||
仅收集只读属性,忽略访问抛出异常的属性。
|
||||
"""
|
||||
properties_bucket = self._metadata.setdefault("properties", Properties())
|
||||
for cls in self.__class__.__mro__:
|
||||
for attribute_name, attribute_value in cls.__dict__.items():
|
||||
if attribute_name.startswith("_"):
|
||||
continue
|
||||
if isinstance(attribute_value, property):
|
||||
try:
|
||||
# 避免重复收集已存在的属性
|
||||
if attribute_name not in properties_bucket:
|
||||
properties_bucket[attribute_name] = getattr(self, attribute_name) # type: ignore[literal-required]
|
||||
except (AttributeError, TypeError, RuntimeError):
|
||||
# Skip attributes that can't be accessed or have incompatible types
|
||||
# This includes attributes that require initialization to complete (like 'fields')
|
||||
pass
|
||||
|
||||
def to(self, target_type: MessageType, **kwargs: Any) -> "MessageInstance[Any]":
|
||||
"""直接转换到目标类型"""
|
||||
if target_type == MessageType.ROS2:
|
||||
return cast("MessageInstance[Any]", self.to_ros2(**kwargs))
|
||||
elif target_type == MessageType.DICT:
|
||||
return cast("MessageInstance[Any]", self.to_dict(**kwargs))
|
||||
elif target_type == MessageType.JSON:
|
||||
return cast("MessageInstance[Any]", self.to_json(**kwargs))
|
||||
elif target_type == MessageType.JSON_SCHEMA:
|
||||
return cast("MessageInstance[Any]", self.to_json_schema(**kwargs))
|
||||
elif target_type == MessageType.YAML:
|
||||
return cast("MessageInstance[Any]", self.to_yaml(**kwargs))
|
||||
elif target_type == MessageType.PYDANTIC:
|
||||
return cast("MessageInstance[Any]", self.to_pydantic(**kwargs))
|
||||
elif target_type == MessageType.DATACLASS:
|
||||
return cast("MessageInstance[Any]", self.to_dataclass(**kwargs))
|
||||
else:
|
||||
raise ValueError(f"Unsupported target message type: {target_type}")
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def import_from_envelope(cls, data: MessageEnvelope, **kwargs: Any) -> "MessageInstance[Any]":
|
||||
"""从统一信封字典创建实例(仅接受 data 一个参数)。"""
|
||||
# metadata会被重置
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def export_to_envelope(self, **kwargs: Any) -> MessageEnvelope:
|
||||
"""导出为字典格式"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_python_dict(self) -> Dict[str, Any]:
|
||||
"""获取当前所有的字段名和对应的python可读值"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_python_dict(self, value: Dict[str, Any], **kwargs: Any) -> bool:
|
||||
"""设置所有字段的值"""
|
||||
pass
|
||||
|
||||
def get_json_schema(self) -> Dict[str, Any]:
|
||||
"""生成当前消息实例的JSON Schema,委托给FieldAccessor递归处理"""
|
||||
# 直接调用FieldAccessor的get_json_schema方法
|
||||
schema = self.fields.get_json_schema()
|
||||
|
||||
# 添加schema元信息(对于JSONSchemaMessageInstance,如果已有title则保持,否则添加默认title)
|
||||
from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
|
||||
|
||||
if isinstance(self, JSONSchemaMessageInstance):
|
||||
# 对于JSON Schema实例,如果schema中没有title,则添加一个
|
||||
if "title" not in schema:
|
||||
schema["title"] = f"{self.__class__.__name__} Schema" # type: ignore
|
||||
if "description" not in schema:
|
||||
schema["description"] = f"JSON Schema generated from {self.message_type.value} message instance" # type: ignore
|
||||
else:
|
||||
# 对于其他类型的实例,总是添加schema元信息
|
||||
schema["title"] = f"{self.__class__.__name__} Schema" # type: ignore
|
||||
schema["description"] = f"JSON Schema generated from {self.message_type.value} message instance" # type: ignore
|
||||
|
||||
return schema
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(type={self.message_type.value}, id={self._instance_id[:8]})"
|
||||
|
||||
# 便捷转换方法,使用MessageCenter单例
|
||||
def to_ros2(self, type_hint: str | Type[Any], **kwargs: Any) -> "ROS2MessageInstance":
|
||||
"""转换到ROS2实例。传入必备的类型提示,"""
|
||||
override_properties = {}
|
||||
from msgcenterpy.core.message_center import get_message_center
|
||||
|
||||
ros2_message_instance = cast(
|
||||
ROS2MessageInstance,
|
||||
get_message_center().get_instance_class(MessageType.ROS2),
|
||||
)
|
||||
ros_type = ros2_message_instance.obtain_ros_cls_from_str(type_hint)
|
||||
override_properties["ros_msg_cls_path"] = ROS2MessageInstance.get_ros_msg_cls_path(ros_type)
|
||||
override_properties["ros_msg_cls_namespace"] = ROS2MessageInstance.get_ros_msg_cls_namespace(ros_type)
|
||||
return cast(
|
||||
ROS2MessageInstance,
|
||||
get_message_center().convert(self, MessageType.ROS2, override_properties, **kwargs),
|
||||
)
|
||||
|
||||
def to_json_schema(self, **kwargs: Any) -> "JSONSchemaMessageInstance":
|
||||
"""转换到JSON Schema实例"""
|
||||
override_properties = {"json_schema": self.get_json_schema()}
|
||||
from msgcenterpy.core.message_center import get_message_center
|
||||
from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
|
||||
|
||||
return cast(
|
||||
JSONSchemaMessageInstance,
|
||||
get_message_center().convert(self, MessageType.JSON_SCHEMA, override_properties, **kwargs),
|
||||
)
|
||||
411
msgcenterpy/core/type_converter.py
Normal file
411
msgcenterpy/core/type_converter.py
Normal file
@@ -0,0 +1,411 @@
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Type, Union, get_args, get_origin
|
||||
|
||||
|
||||
class StandardType(Enum):
|
||||
"""标准化的数据类型,用于不同数据源之间的转换
|
||||
|
||||
增强版本,提供更细粒度的类型保留以更好地保存原始类型信息
|
||||
"""
|
||||
|
||||
# 基础类型
|
||||
STRING = "string" # 字符串类型
|
||||
WSTRING = "wstring" # 宽字符串类型
|
||||
CHAR = "char" # 字符类型
|
||||
WCHAR = "wchar" # 宽字符类型
|
||||
|
||||
# 整数类型(细分以保留精度信息)
|
||||
INT8 = "int8" # 8位有符号整数
|
||||
UINT8 = "uint8" # 8位无符号整数
|
||||
INT16 = "int16" # 16位有符号整数
|
||||
UINT16 = "uint16" # 16位无符号整数
|
||||
INT32 = "int32" # 32位有符号整数
|
||||
UINT32 = "uint32" # 32位无符号整数
|
||||
INT64 = "int64" # 64位有符号整数
|
||||
UINT64 = "uint64" # 64位无符号整数
|
||||
INTEGER = "integer" # 通用整数类型(向后兼容)
|
||||
BYTE = "byte" # 字节类型
|
||||
OCTET = "octet" # 八位字节类型
|
||||
|
||||
# 浮点类型(细分以保留精度信息)
|
||||
FLOAT32 = "float32" # 32位浮点数
|
||||
FLOAT64 = "float64" # 64位浮点数(双精度)
|
||||
DOUBLE = "double" # 双精度浮点数
|
||||
FLOAT = "float" # 通用浮点类型(向后兼容)
|
||||
|
||||
# 布尔类型
|
||||
BOOLEAN = "boolean" # 布尔类型
|
||||
BOOL = "bool" # 布尔类型(ROS2风格)
|
||||
|
||||
# 空值类型
|
||||
NULL = "null" # 空值类型
|
||||
|
||||
# 容器类型
|
||||
ARRAY = "array" # 数组/序列类型
|
||||
BOUNDED_ARRAY = "bounded_array" # 有界数组类型
|
||||
UNBOUNDED_ARRAY = "unbounded_array" # 无界数组类型
|
||||
SEQUENCE = "sequence" # 序列类型
|
||||
BOUNDED_SEQUENCE = "bounded_sequence" # 有界序列类型
|
||||
UNBOUNDED_SEQUENCE = "unbounded_sequence" # 无界序列类型
|
||||
OBJECT = "object" # 对象/映射类型
|
||||
|
||||
# 扩展类型
|
||||
DATETIME = "datetime" # 日期时间类型
|
||||
TIME = "time" # 时间类型
|
||||
DURATION = "duration" # 持续时间类型
|
||||
BYTES = "bytes" # 字节数据类型
|
||||
DECIMAL = "decimal" # 精确小数类型
|
||||
|
||||
# 特殊类型
|
||||
UNKNOWN = "unknown" # 未知类型
|
||||
ANY = "any" # 任意类型
|
||||
|
||||
@property
|
||||
def IS_ARRAY(self) -> bool:
|
||||
"""判断该类型是否为数组/序列类型"""
|
||||
array_like = {
|
||||
StandardType.ARRAY,
|
||||
StandardType.BOUNDED_ARRAY,
|
||||
StandardType.UNBOUNDED_ARRAY,
|
||||
StandardType.SEQUENCE,
|
||||
StandardType.BOUNDED_SEQUENCE,
|
||||
StandardType.UNBOUNDED_SEQUENCE,
|
||||
}
|
||||
return self in array_like
|
||||
|
||||
|
||||
class TypeConverter:
|
||||
"""类型转换器,负责不同数据源类型之间的转换和标准化"""
|
||||
|
||||
# Python基础类型到标准类型的映射
|
||||
PYTHON_TO_STANDARD = {
|
||||
str: StandardType.STRING,
|
||||
int: StandardType.INTEGER, # 保持向后兼容的通用整数
|
||||
float: StandardType.FLOAT, # 保持向后兼容的通用浮点
|
||||
bool: StandardType.BOOLEAN,
|
||||
type(None): StandardType.NULL,
|
||||
list: StandardType.ARRAY,
|
||||
tuple: StandardType.ARRAY,
|
||||
dict: StandardType.OBJECT,
|
||||
datetime: StandardType.DATETIME,
|
||||
bytes: StandardType.BYTES,
|
||||
bytearray: StandardType.BYTES,
|
||||
Decimal: StandardType.DECIMAL,
|
||||
}
|
||||
|
||||
# 标准类型到Python类型的映射
|
||||
STANDARD_TO_PYTHON = {
|
||||
# 字符串类型
|
||||
StandardType.STRING: str,
|
||||
StandardType.WSTRING: str,
|
||||
StandardType.CHAR: str,
|
||||
StandardType.WCHAR: str,
|
||||
# 整数类型(都映射到int,Python会自动处理范围)
|
||||
StandardType.INT8: int,
|
||||
StandardType.UINT8: int,
|
||||
StandardType.INT16: int,
|
||||
StandardType.UINT16: int,
|
||||
StandardType.INT32: int,
|
||||
StandardType.UINT32: int,
|
||||
StandardType.INT64: int,
|
||||
StandardType.UINT64: int,
|
||||
StandardType.INTEGER: int,
|
||||
StandardType.BYTE: int,
|
||||
StandardType.OCTET: int,
|
||||
# 浮点类型
|
||||
StandardType.FLOAT32: float,
|
||||
StandardType.FLOAT64: float,
|
||||
StandardType.DOUBLE: float,
|
||||
StandardType.FLOAT: float,
|
||||
# 布尔类型
|
||||
StandardType.BOOLEAN: bool,
|
||||
StandardType.BOOL: bool,
|
||||
# 空值类型
|
||||
StandardType.NULL: type(None),
|
||||
# 容器类型
|
||||
StandardType.ARRAY: list,
|
||||
StandardType.BOUNDED_ARRAY: list,
|
||||
StandardType.UNBOUNDED_ARRAY: list,
|
||||
StandardType.SEQUENCE: list,
|
||||
StandardType.BOUNDED_SEQUENCE: list,
|
||||
StandardType.UNBOUNDED_SEQUENCE: list,
|
||||
StandardType.OBJECT: dict,
|
||||
# 扩展类型
|
||||
StandardType.DATETIME: datetime,
|
||||
StandardType.TIME: datetime,
|
||||
StandardType.DURATION: float, # 持续时间用秒表示
|
||||
StandardType.BYTES: bytes,
|
||||
StandardType.DECIMAL: Decimal,
|
||||
# 特殊类型
|
||||
StandardType.UNKNOWN: object,
|
||||
StandardType.ANY: object,
|
||||
}
|
||||
|
||||
# ROS2类型到标准类型的映射(保留原始类型精度)
|
||||
ROS2_TO_STANDARD = {
|
||||
# 字符串类型(保留具体类型)
|
||||
"string": StandardType.STRING,
|
||||
"wstring": StandardType.WSTRING,
|
||||
"char": StandardType.CHAR,
|
||||
"wchar": StandardType.WCHAR,
|
||||
# 整数类型(保留精度信息)
|
||||
"int8": StandardType.INT8,
|
||||
"uint8": StandardType.UINT8,
|
||||
"int16": StandardType.INT16,
|
||||
"short": StandardType.INT16, # to check
|
||||
"uint16": StandardType.UINT16,
|
||||
"unsigned short": StandardType.UINT16, # to check
|
||||
"int32": StandardType.INT32,
|
||||
"uint32": StandardType.UINT32,
|
||||
"int64": StandardType.INT64,
|
||||
"long": StandardType.INT64, # to check
|
||||
"long long": StandardType.INT64, # to check
|
||||
"uint64": StandardType.UINT64,
|
||||
"unsigned long": StandardType.UINT64, # to check
|
||||
"unsigned long long": StandardType.UINT64, # to check
|
||||
"byte": StandardType.BYTE,
|
||||
"octet": StandardType.OCTET,
|
||||
# 浮点类型(保留精度信息)
|
||||
"float32": StandardType.FLOAT32,
|
||||
"float64": StandardType.FLOAT64,
|
||||
"double": StandardType.DOUBLE,
|
||||
"long double": StandardType.DOUBLE,
|
||||
"float": StandardType.FLOAT32, # 默认为32位
|
||||
# 布尔类型
|
||||
"bool": StandardType.BOOL,
|
||||
"boolean": StandardType.BOOLEAN,
|
||||
# 时间和持续时间(更精确的类型映射)
|
||||
"time": StandardType.TIME,
|
||||
"duration": StandardType.DURATION,
|
||||
# 向后兼容的通用映射(当需要时可以回退到这些)
|
||||
"generic_int": StandardType.INTEGER,
|
||||
"generic_float": StandardType.FLOAT,
|
||||
"generic_bool": StandardType.BOOLEAN,
|
||||
"generic_string": StandardType.STRING,
|
||||
}
|
||||
|
||||
# JSON Schema类型到标准类型的映射
|
||||
JSON_SCHEMA_TO_STANDARD = {
|
||||
"string": StandardType.STRING,
|
||||
"integer": StandardType.INTEGER,
|
||||
"number": StandardType.FLOAT,
|
||||
"boolean": StandardType.BOOLEAN,
|
||||
"null": StandardType.NULL,
|
||||
"array": StandardType.ARRAY,
|
||||
"object": StandardType.OBJECT,
|
||||
}
|
||||
|
||||
# 标准类型到Python类型的映射
|
||||
STANDARD_TO_JSON_SCHEMA = {
|
||||
# 字符串类型
|
||||
StandardType.STRING: "string",
|
||||
StandardType.WSTRING: "string",
|
||||
StandardType.CHAR: "string",
|
||||
StandardType.WCHAR: "string",
|
||||
# 整数类型(都映射到int,Python会自动处理范围)
|
||||
StandardType.INT8: "integer",
|
||||
StandardType.UINT8: "integer",
|
||||
StandardType.INT16: "integer",
|
||||
StandardType.UINT16: "integer",
|
||||
StandardType.INT32: "integer",
|
||||
StandardType.UINT32: "integer",
|
||||
StandardType.INT64: "integer",
|
||||
StandardType.UINT64: "integer",
|
||||
StandardType.INTEGER: "integer",
|
||||
StandardType.BYTE: "integer",
|
||||
StandardType.OCTET: "integer",
|
||||
# 浮点类型
|
||||
StandardType.FLOAT32: "number",
|
||||
StandardType.FLOAT64: "number",
|
||||
StandardType.DOUBLE: "number",
|
||||
StandardType.FLOAT: "number",
|
||||
# 布尔类型
|
||||
StandardType.BOOLEAN: "boolean",
|
||||
StandardType.BOOL: "boolean",
|
||||
# 空值类型
|
||||
StandardType.NULL: "null",
|
||||
# 容器类型
|
||||
StandardType.ARRAY: "array",
|
||||
StandardType.BOUNDED_ARRAY: "array",
|
||||
StandardType.UNBOUNDED_ARRAY: "array",
|
||||
StandardType.SEQUENCE: "array",
|
||||
StandardType.BOUNDED_SEQUENCE: "array",
|
||||
StandardType.UNBOUNDED_SEQUENCE: "array",
|
||||
StandardType.OBJECT: "object",
|
||||
# 扩展类型
|
||||
StandardType.DATETIME: "string", # 在JSON Schema中日期时间通常表示为字符串
|
||||
StandardType.TIME: "string",
|
||||
StandardType.DURATION: "number",
|
||||
StandardType.BYTES: "string", # 字节数据在JSON Schema中通常表示为base64字符串
|
||||
StandardType.DECIMAL: "number",
|
||||
# 特殊类型
|
||||
StandardType.UNKNOWN: "string",
|
||||
StandardType.ANY: "string",
|
||||
}
|
||||
|
||||
# Array typecode到标准类型的映射(更精确的类型保留)
|
||||
ARRAY_TYPECODE_TO_STANDARD = {
|
||||
"b": StandardType.INT8, # signed char
|
||||
"B": StandardType.UINT8, # unsigned char
|
||||
"h": StandardType.INT16, # signed short
|
||||
"H": StandardType.UINT16, # unsigned short
|
||||
"i": StandardType.INT32, # signed int
|
||||
"I": StandardType.UINT32, # unsigned int
|
||||
"l": StandardType.INT64, # signed long
|
||||
"L": StandardType.UINT64, # unsigned long
|
||||
"f": StandardType.FLOAT32, # float
|
||||
"d": StandardType.FLOAT64, # double
|
||||
}
|
||||
|
||||
# Array typecode到Python类型的映射
|
||||
ARRAY_TYPECODE_TO_PYTHON = {
|
||||
"b": int, # signed char
|
||||
"B": int, # unsigned char
|
||||
"h": int, # signed short
|
||||
"H": int, # unsigned short
|
||||
"i": int, # signed int
|
||||
"I": int, # unsigned int
|
||||
"l": int, # signed long
|
||||
"L": int, # unsigned long
|
||||
"f": float, # float
|
||||
"d": float, # double
|
||||
}
|
||||
|
||||
"""Python Type"""
|
||||
|
||||
@classmethod
|
||||
def python_type_to_standard(cls, python_type: Type) -> StandardType:
|
||||
"""将Python类型转换为标准类型"""
|
||||
# 处理泛型类型
|
||||
origin = get_origin(python_type)
|
||||
if origin is not None:
|
||||
if origin in (list, tuple):
|
||||
return StandardType.ARRAY
|
||||
elif origin is dict:
|
||||
return StandardType.OBJECT
|
||||
elif origin in (Union, type(Union[int, None])):
|
||||
# 处理Optional类型和Union类型
|
||||
args = get_args(python_type)
|
||||
non_none_types = [arg for arg in args if arg != type(None)]
|
||||
if len(non_none_types) == 1:
|
||||
return cls.python_type_to_standard(non_none_types[0])
|
||||
return StandardType.ANY
|
||||
|
||||
# 处理基础类型
|
||||
return cls.PYTHON_TO_STANDARD.get(python_type, StandardType.UNKNOWN)
|
||||
|
||||
@classmethod
|
||||
def standard_to_python_type(cls, standard_type: StandardType) -> Type:
|
||||
"""将标准类型转换为Python类型"""
|
||||
return cls.STANDARD_TO_PYTHON.get(standard_type, object)
|
||||
|
||||
"""ROS2"""
|
||||
|
||||
@classmethod
|
||||
def ros2_type_str_to_standard(cls, ros2_type_str: str) -> StandardType:
|
||||
"""将ROS2类型字符串转换为标准类型"""
|
||||
if "[" in ros2_type_str and "]" in ros2_type_str:
|
||||
return StandardType.ARRAY
|
||||
base_type = ros2_type_str.split("/")[-1].lower()
|
||||
return cls.ROS2_TO_STANDARD.get(base_type, StandardType.UNKNOWN)
|
||||
|
||||
@classmethod
|
||||
def rosidl_definition_to_standard(cls, definition_type: Any) -> StandardType:
|
||||
from rosidl_parser.definition import ( # type: ignore
|
||||
Array,
|
||||
BasicType,
|
||||
BoundedSequence,
|
||||
BoundedString,
|
||||
BoundedWString,
|
||||
NamedType,
|
||||
NamespacedType,
|
||||
UnboundedSequence,
|
||||
UnboundedString,
|
||||
UnboundedWString,
|
||||
)
|
||||
|
||||
# 基础类型转换(保留原始类型精度)
|
||||
if isinstance(definition_type, BasicType):
|
||||
type_name = definition_type.typename.lower()
|
||||
return cls.ros2_type_str_to_standard(type_name)
|
||||
# 字符串类型(区分普通字符串和宽字符串)
|
||||
elif isinstance(definition_type, (UnboundedString, BoundedString)):
|
||||
return StandardType.STRING
|
||||
elif isinstance(definition_type, (UnboundedWString, BoundedWString)):
|
||||
return StandardType.WSTRING
|
||||
# 数组和序列类型(更精确的类型区分)
|
||||
elif isinstance(definition_type, Array):
|
||||
return StandardType.BOUNDED_ARRAY
|
||||
elif isinstance(definition_type, UnboundedSequence):
|
||||
return StandardType.UNBOUNDED_SEQUENCE
|
||||
elif isinstance(definition_type, BoundedSequence):
|
||||
return StandardType.BOUNDED_SEQUENCE
|
||||
# 命名类型和命名空间类型统一为OBJECT
|
||||
elif isinstance(definition_type, (NamedType, NamespacedType)):
|
||||
return StandardType.OBJECT
|
||||
# 未知类型
|
||||
else:
|
||||
return StandardType.UNKNOWN
|
||||
|
||||
@classmethod
|
||||
def array_typecode_to_standard(cls, typecode: str) -> StandardType:
|
||||
"""将array.array的typecode转换为标准类型"""
|
||||
return cls.ARRAY_TYPECODE_TO_STANDARD.get(typecode, StandardType.UNKNOWN)
|
||||
|
||||
"""JSON Schema"""
|
||||
|
||||
@classmethod
|
||||
def json_schema_type_to_standard(cls, json_type: str) -> StandardType:
|
||||
"""将JSON Schema类型转换为标准类型"""
|
||||
return cls.JSON_SCHEMA_TO_STANDARD.get(json_type, StandardType.UNKNOWN)
|
||||
|
||||
@classmethod
|
||||
def standard_type_to_json_schema_type(cls, standard_type: StandardType) -> str:
|
||||
"""将StandardType转换为JSON Schema类型字符串"""
|
||||
return cls.STANDARD_TO_JSON_SCHEMA.get(standard_type, "string")
|
||||
|
||||
"""值转换"""
|
||||
|
||||
@classmethod
|
||||
def convert_to_python_value_with_standard_type(cls, value: Any, target_standard_type: StandardType) -> Any:
|
||||
"""将值转换为指定的标准类型对应的Python值"""
|
||||
if value is None:
|
||||
return None if target_standard_type == StandardType.NULL else None
|
||||
target_python_type = cls.standard_to_python_type(target_standard_type)
|
||||
if target_python_type != object and type(value) == target_python_type:
|
||||
# object交由target_standard_type为OBJECT的分支处理,同样返回原值
|
||||
return value
|
||||
if target_standard_type == StandardType.ARRAY:
|
||||
if isinstance(value, (list, tuple)):
|
||||
return list(value)
|
||||
elif hasattr(value, "typecode"): # array.array
|
||||
return list(value)
|
||||
elif isinstance(value, str):
|
||||
return list(value) # 字符串转为字符数组
|
||||
else:
|
||||
return [value] # 单个值包装为数组
|
||||
elif target_standard_type == StandardType.OBJECT:
|
||||
return value
|
||||
elif target_standard_type == StandardType.DATETIME:
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
elif isinstance(value, (int, float)):
|
||||
return datetime.fromtimestamp(value)
|
||||
elif isinstance(value, str):
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
else:
|
||||
return datetime.now()
|
||||
elif target_standard_type == StandardType.BYTES:
|
||||
if isinstance(value, bytes):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
return value.encode("utf-8")
|
||||
elif isinstance(value, (list, tuple)):
|
||||
return bytes(value)
|
||||
else:
|
||||
return str(value).encode("utf-8")
|
||||
else:
|
||||
# 基础类型转换
|
||||
return target_python_type(value)
|
||||
400
msgcenterpy/core/type_info.py
Normal file
400
msgcenterpy/core/type_info.py
Normal file
@@ -0,0 +1,400 @@
|
||||
import copy
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from msgcenterpy.core.type_converter import StandardType, TypeConverter
|
||||
|
||||
|
||||
class Consts:
|
||||
ELEMENT_TYPE_INFO_SYMBOL = "ELEMENT_TYPE_INFO"
|
||||
ACCESSOR_ROOT_NODE = "MSG_CENTER_ROOT"
|
||||
|
||||
|
||||
class ConstraintType(Enum):
|
||||
"""Constraint type enumeration"""
|
||||
|
||||
MIN_VALUE = "min_value"
|
||||
MAX_VALUE = "max_value"
|
||||
MIN_LENGTH = "min_length"
|
||||
MAX_LENGTH = "max_length"
|
||||
MIN_ITEMS = "min_items"
|
||||
MAX_ITEMS = "max_items"
|
||||
PATTERN = "pattern"
|
||||
ENUM_VALUES = "enum_values"
|
||||
MULTIPLE_OF = "multiple_of"
|
||||
TYPE_KEEP = "type_keep"
|
||||
EXCLUSIVE_MIN = "exclusive_min"
|
||||
EXCLUSIVE_MAX = "exclusive_max"
|
||||
UNIQUE_ITEMS = "unique_items"
|
||||
DEFAULT_VALUE = "default_value"
|
||||
REQUIRED = "required"
|
||||
FORMAT = "format"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TypeConstraint:
|
||||
"""Type constraint definition"""
|
||||
|
||||
type: ConstraintType
|
||||
value: Any
|
||||
description: Optional[str] = None
|
||||
|
||||
def to_json_schema_property(self) -> Dict[str, Any]:
|
||||
"""Convert to JSON Schema property"""
|
||||
mapping = {
|
||||
ConstraintType.MIN_VALUE: "minimum",
|
||||
ConstraintType.MAX_VALUE: "maximum",
|
||||
ConstraintType.MIN_LENGTH: "minLength",
|
||||
ConstraintType.MAX_LENGTH: "maxLength",
|
||||
ConstraintType.MIN_ITEMS: "minItems",
|
||||
ConstraintType.MAX_ITEMS: "maxItems",
|
||||
ConstraintType.PATTERN: "pattern",
|
||||
ConstraintType.ENUM_VALUES: "enum",
|
||||
ConstraintType.MULTIPLE_OF: "multipleOf",
|
||||
ConstraintType.EXCLUSIVE_MIN: "exclusiveMinimum",
|
||||
ConstraintType.EXCLUSIVE_MAX: "exclusiveMaximum",
|
||||
ConstraintType.UNIQUE_ITEMS: "uniqueItems",
|
||||
ConstraintType.DEFAULT_VALUE: "default",
|
||||
ConstraintType.FORMAT: "format",
|
||||
}
|
||||
|
||||
property_name = mapping.get(self.type)
|
||||
if property_name:
|
||||
result = {property_name: self.value}
|
||||
if self.description:
|
||||
result["description"] = self.description
|
||||
return result
|
||||
return {}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TypeInfo:
|
||||
"""Detailed type information including standard type, Python type and constraints"""
|
||||
|
||||
# Basic type information
|
||||
field_name: str
|
||||
field_path: str
|
||||
standard_type: StandardType
|
||||
python_type: Type
|
||||
original_type: Any # Original type (e.g., ROS2 type instance)
|
||||
_outdated: bool = False
|
||||
|
||||
@property
|
||||
def outdated(self) -> bool:
|
||||
return self._outdated
|
||||
|
||||
# Value information
|
||||
current_value: Any = None
|
||||
default_value: Any = None
|
||||
|
||||
# Constraints
|
||||
constraints: List[TypeConstraint] = field(default_factory=list)
|
||||
|
||||
# Array/sequence related information
|
||||
is_array: bool = False
|
||||
array_size: Optional[int] = None # Fixed size array
|
||||
_element_type_info: Optional["TypeInfo"] = None # Array element type
|
||||
|
||||
@property
|
||||
def element_type_info(self) -> Optional["TypeInfo"]:
|
||||
return self._element_type_info
|
||||
|
||||
@element_type_info.setter
|
||||
def element_type_info(self, value: Optional["TypeInfo"]) -> None:
|
||||
if self.outdated:
|
||||
raise ValueError("Should not change an outdated type")
|
||||
if value is not None:
|
||||
value.field_name = Consts.ELEMENT_TYPE_INFO_SYMBOL
|
||||
value.field_path = Consts.ELEMENT_TYPE_INFO_SYMBOL
|
||||
self._element_type_info = value
|
||||
|
||||
# Object related information
|
||||
is_object: bool = False
|
||||
object_fields: Dict[str, "TypeInfo"] = field(default_factory=dict)
|
||||
|
||||
# Additional metadata
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def python_value_from_standard_type(self) -> Any:
|
||||
return TypeConverter.convert_to_python_value_with_standard_type(self.current_value, self.standard_type)
|
||||
|
||||
def add_constraint(
|
||||
self,
|
||||
constraint_type: ConstraintType,
|
||||
value: Any,
|
||||
description: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Add constraint"""
|
||||
constraint = TypeConstraint(constraint_type, value, description)
|
||||
# Avoid duplicate constraints of the same type
|
||||
self.constraints = [c for c in self.constraints if c.type != constraint_type]
|
||||
self.constraints.append(constraint)
|
||||
|
||||
def get_constraint(self, constraint_type: ConstraintType) -> Optional[TypeConstraint]:
|
||||
"""Get constraint of specified type"""
|
||||
for constraint in self.constraints:
|
||||
if constraint.type == constraint_type:
|
||||
return constraint
|
||||
return None
|
||||
|
||||
def has_constraint(self, constraint_type: ConstraintType) -> bool:
|
||||
"""Check if constraint of specified type exists"""
|
||||
return self.get_constraint(constraint_type) is not None
|
||||
|
||||
def get_constraint_value(self, constraint_type: ConstraintType) -> Any:
|
||||
"""Get value of specified constraint"""
|
||||
constraint = self.get_constraint(constraint_type)
|
||||
return constraint.value if constraint else None
|
||||
|
||||
def validate_value(self, value: Any) -> bool:
|
||||
"""Validate value according to constraints"""
|
||||
try:
|
||||
if self.get_constraint(ConstraintType.TYPE_KEEP):
|
||||
# ROS includes TYPE_KEEP
|
||||
if type(self.current_value) != type(value):
|
||||
return False
|
||||
# Basic type check
|
||||
if not self._validate_basic_type(value):
|
||||
return False
|
||||
|
||||
# Numeric constraint check
|
||||
if not self._validate_numeric_constraints(value):
|
||||
return False
|
||||
|
||||
# String constraint check
|
||||
if not self._validate_string_constraints(value):
|
||||
return False
|
||||
|
||||
# Array constraint check
|
||||
if not self._validate_array_constraints(value):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _validate_basic_type(self, value: Any) -> bool:
|
||||
"""Validate basic type"""
|
||||
if value is None:
|
||||
return not self.has_constraint(ConstraintType.REQUIRED)
|
||||
return True
|
||||
|
||||
def _validate_numeric_constraints(self, value: Any) -> bool:
|
||||
"""Validate numeric constraints"""
|
||||
if not isinstance(value, (int, float)):
|
||||
return True
|
||||
|
||||
min_val = self.get_constraint_value(ConstraintType.MIN_VALUE)
|
||||
if min_val is not None and value < min_val:
|
||||
return False
|
||||
|
||||
max_val = self.get_constraint_value(ConstraintType.MAX_VALUE)
|
||||
if max_val is not None and value > max_val:
|
||||
return False
|
||||
|
||||
exclusive_min = self.get_constraint_value(ConstraintType.EXCLUSIVE_MIN)
|
||||
if exclusive_min is not None and value <= exclusive_min:
|
||||
return False
|
||||
|
||||
exclusive_max = self.get_constraint_value(ConstraintType.EXCLUSIVE_MAX)
|
||||
if exclusive_max is not None and value >= exclusive_max:
|
||||
return False
|
||||
|
||||
multiple_of = self.get_constraint_value(ConstraintType.MULTIPLE_OF)
|
||||
if multiple_of is not None and value % multiple_of != 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _validate_string_constraints(self, value: Any) -> bool:
|
||||
"""Validate string constraints"""
|
||||
if not isinstance(value, str):
|
||||
return True
|
||||
|
||||
min_len = self.get_constraint_value(ConstraintType.MIN_LENGTH)
|
||||
if min_len is not None and len(value) < min_len:
|
||||
return False
|
||||
|
||||
max_len = self.get_constraint_value(ConstraintType.MAX_LENGTH)
|
||||
if max_len is not None and len(value) > max_len:
|
||||
return False
|
||||
|
||||
pattern = self.get_constraint_value(ConstraintType.PATTERN)
|
||||
if pattern is not None:
|
||||
import re
|
||||
|
||||
if not re.match(pattern, value):
|
||||
return False
|
||||
|
||||
enum_values = self.get_constraint_value(ConstraintType.ENUM_VALUES)
|
||||
if enum_values is not None and value not in enum_values:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _validate_array_constraints(self, value: Any) -> bool:
|
||||
"""Validate array constraints"""
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return True
|
||||
|
||||
min_items = self.get_constraint_value(ConstraintType.MIN_ITEMS)
|
||||
if min_items is not None and len(value) < min_items:
|
||||
return False
|
||||
|
||||
max_items = self.get_constraint_value(ConstraintType.MAX_ITEMS)
|
||||
if max_items is not None and len(value) > max_items:
|
||||
return False
|
||||
|
||||
if self.array_size is not None and len(value) != self.array_size:
|
||||
return False
|
||||
|
||||
unique_items = self.get_constraint_value(ConstraintType.UNIQUE_ITEMS)
|
||||
if unique_items and len(set(value)) != len(value):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def to_json_schema_property(self, include_constraints: bool = True) -> Dict[str, Any]:
|
||||
"""Convert to JSON Schema property definition"""
|
||||
from msgcenterpy.core.type_converter import TypeConverter
|
||||
|
||||
# Basic properties
|
||||
property_schema: Dict[str, Any] = {"type": TypeConverter.standard_type_to_json_schema_type(self.standard_type)}
|
||||
|
||||
# Add constraints
|
||||
if include_constraints:
|
||||
for constraint in self.constraints:
|
||||
constraint_props = constraint.to_json_schema_property()
|
||||
property_schema.update(constraint_props)
|
||||
|
||||
# Special handling for array types
|
||||
if self.is_array and self.element_type_info:
|
||||
property_schema["items"] = self.element_type_info.to_json_schema_property(include_constraints)
|
||||
|
||||
# Special handling for object types
|
||||
if self.is_object and self.object_fields:
|
||||
properties = {}
|
||||
for field_name, field_info in self.object_fields.items():
|
||||
properties[field_name] = field_info.to_json_schema_property(include_constraints)
|
||||
property_schema["properties"] = properties
|
||||
|
||||
# Add description
|
||||
if self.original_type:
|
||||
property_schema["description"] = f"Field of type {self.original_type}"
|
||||
|
||||
return property_schema
|
||||
|
||||
def convert_value(self, value: Any, target_standard_type: Optional[StandardType] = None) -> Any:
|
||||
"""Convert value to current type or specified target type"""
|
||||
target_type = target_standard_type or self.standard_type
|
||||
converted_value = TypeConverter.convert_to_python_value_with_standard_type(value, target_type)
|
||||
# Validate converted value
|
||||
if target_standard_type is None and not self.validate_value(converted_value):
|
||||
# Format constraint information
|
||||
constraints_info = []
|
||||
for c in self.constraints:
|
||||
constraint_desc = f"{c.type.value}: {c.value}"
|
||||
if c.description:
|
||||
constraint_desc += f" ({c.description})"
|
||||
constraints_info.append(constraint_desc)
|
||||
|
||||
constraints_str = ", ".join(constraints_info) if constraints_info else "No constraints"
|
||||
raise ValueError(
|
||||
f"Value {value} does not meet constraints for field {self.field_name}. "
|
||||
f"Constraints: [{constraints_str}]"
|
||||
)
|
||||
return converted_value
|
||||
|
||||
def get_value_info(self) -> Dict[str, Any]:
|
||||
"""Get detailed information about current value"""
|
||||
return {
|
||||
"field_name": self.field_name,
|
||||
"current_value": self.current_value,
|
||||
"standard_type": self.standard_type.value,
|
||||
"python_type": self.python_type.__name__,
|
||||
"original_type": self.original_type,
|
||||
"is_valid": self.validate_value(self.current_value),
|
||||
"constraints": [
|
||||
{"type": c.type.value, "value": c.value, "description": c.description} for c in self.constraints
|
||||
],
|
||||
"is_array": self.is_array,
|
||||
"array_size": self.array_size,
|
||||
"is_object": self.is_object,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
def clone(self) -> "TypeInfo":
|
||||
"""Create deep copy of TypeInfo"""
|
||||
return copy.deepcopy(self)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
constraints_str = f", {len(self.constraints)} constraints" if self.constraints else ""
|
||||
return f"TypeInfo({self.field_name}: {self.standard_type.value}{constraints_str})"
|
||||
|
||||
|
||||
class TypeInfoPostProcessor:
|
||||
"""TypeInfo post-processor that adds default constraints to TypeInfo"""
|
||||
|
||||
@staticmethod
|
||||
def add_basic_type_constraints(type_info: TypeInfo) -> None:
|
||||
"""Add range constraints for basic types"""
|
||||
if not type_info.standard_type:
|
||||
return
|
||||
|
||||
standard_type = type_info.standard_type
|
||||
|
||||
# Integer type range constraints
|
||||
if standard_type == StandardType.INT8:
|
||||
type_info.add_constraint(ConstraintType.MIN_VALUE, -128)
|
||||
type_info.add_constraint(ConstraintType.MAX_VALUE, 127)
|
||||
elif standard_type in (
|
||||
StandardType.UINT8,
|
||||
StandardType.BYTE,
|
||||
StandardType.OCTET,
|
||||
):
|
||||
type_info.add_constraint(ConstraintType.MIN_VALUE, 0)
|
||||
type_info.add_constraint(ConstraintType.MAX_VALUE, 255)
|
||||
elif standard_type == StandardType.INT16:
|
||||
type_info.add_constraint(ConstraintType.MIN_VALUE, -32768)
|
||||
type_info.add_constraint(ConstraintType.MAX_VALUE, 32767)
|
||||
elif standard_type == StandardType.UINT16:
|
||||
type_info.add_constraint(ConstraintType.MIN_VALUE, 0)
|
||||
type_info.add_constraint(ConstraintType.MAX_VALUE, 65535)
|
||||
elif standard_type == StandardType.INT32:
|
||||
type_info.add_constraint(ConstraintType.MIN_VALUE, -2147483648)
|
||||
type_info.add_constraint(ConstraintType.MAX_VALUE, 2147483647)
|
||||
elif standard_type == StandardType.UINT32:
|
||||
type_info.add_constraint(ConstraintType.MIN_VALUE, 0)
|
||||
type_info.add_constraint(ConstraintType.MAX_VALUE, 4294967295)
|
||||
elif standard_type == StandardType.INT64:
|
||||
type_info.add_constraint(ConstraintType.MIN_VALUE, -9223372036854775808)
|
||||
type_info.add_constraint(ConstraintType.MAX_VALUE, 9223372036854775807)
|
||||
elif standard_type == StandardType.UINT64:
|
||||
type_info.add_constraint(ConstraintType.MIN_VALUE, 0)
|
||||
type_info.add_constraint(ConstraintType.MAX_VALUE, 18446744073709551615)
|
||||
# Floating point type range constraints
|
||||
elif standard_type in (StandardType.FLOAT, StandardType.FLOAT32):
|
||||
type_info.add_constraint(ConstraintType.MIN_VALUE, -3.4028235e38)
|
||||
type_info.add_constraint(ConstraintType.MAX_VALUE, 3.4028235e38)
|
||||
elif standard_type in (StandardType.DOUBLE, StandardType.FLOAT64):
|
||||
type_info.add_constraint(ConstraintType.MIN_VALUE, -1.7976931348623157e308)
|
||||
type_info.add_constraint(ConstraintType.MAX_VALUE, 1.7976931348623157e308)
|
||||
|
||||
@staticmethod
|
||||
def add_default_constraints(type_info: TypeInfo) -> None:
|
||||
"""Add default constraints"""
|
||||
field_value = type_info.current_value
|
||||
|
||||
# Add constraints for array types
|
||||
if isinstance(field_value, (list, tuple)):
|
||||
type_info.is_array = True
|
||||
# 不再添加冗余的 MIN_ITEMS: 0
|
||||
|
||||
@staticmethod
|
||||
def post_process_type_info(type_info: TypeInfo) -> None:
|
||||
"""Post-process TypeInfo, adding various default constraints"""
|
||||
TypeInfoPostProcessor.add_basic_type_constraints(type_info)
|
||||
TypeInfoPostProcessor.add_default_constraints(type_info)
|
||||
25
msgcenterpy/core/types.py
Normal file
25
msgcenterpy/core/types.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MessageType(Enum):
|
||||
"""Supported message types"""
|
||||
|
||||
ROS2 = "ros2"
|
||||
PYDANTIC = "pydantic"
|
||||
DATACLASS = "dataclass"
|
||||
JSON = "json"
|
||||
JSON_SCHEMA = "json_schema"
|
||||
DICT = "dict"
|
||||
YAML = "yaml"
|
||||
|
||||
|
||||
class ConversionError(Exception):
|
||||
"""Conversion error exception"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""Validation error exception"""
|
||||
|
||||
pass
|
||||
Reference in New Issue
Block a user