init version

This commit is contained in:
Xuwznln
2025-09-02 16:39:44 +08:00
commit 94f0c112e5
41 changed files with 6004 additions and 0 deletions

97
msgcenterpy/__init__.py Normal file
View File

@@ -0,0 +1,97 @@
"""
MsgCenterPy - Unified Message Conversion System
A multi-format message conversion system supporting seamless conversion
between ROS2, Pydantic, Dataclass, JSON, Dict, YAML and JSON Schema.
"""
__version__ = "0.0.2"
__license__ = "Apache-2.0"
from msgcenterpy.core.envelope import MessageEnvelope, create_envelope
from msgcenterpy.core.field_accessor import FieldAccessor
from msgcenterpy.core.message_center import MessageCenter
# Core imports
from msgcenterpy.core.message_instance import MessageInstance
from msgcenterpy.core.type_converter import StandardType, TypeConverter
from msgcenterpy.core.type_info import ConstraintType, TypeInfo
from msgcenterpy.core.types import ConversionError, MessageType, ValidationError
# Always available instance
from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
# Optional ROS2 instance (with graceful fallback)
try:
from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
_HAS_ROS2 = True
except ImportError:
_HAS_ROS2 = False
# Convenience function
def get_message_center() -> MessageCenter:
"""Get the MessageCenter singleton instance."""
return MessageCenter.get_instance()
# Main exports
__all__ = [
# Version info
"__version__",
"__license__",
]
def get_version() -> str:
"""Get the current version of MsgCenterPy."""
return __version__
def get_package_info() -> dict:
"""Get package information."""
return {
"name": "msgcenterpy",
"version": __version__,
"description": "Unified message conversion system supporting ROS2, Pydantic, Dataclass, JSON, YAML, Dict, and JSON Schema inter-conversion",
"license": __license__,
"url": "https://github.com/ZGCA-Forge/MsgCenterPy",
"keywords": [
"message",
"conversion",
"ros2",
"pydantic",
"dataclass",
"json",
"yaml",
"mcp",
],
}
def check_dependencies() -> dict:
"""Check which optional dependencies are available."""
dependencies = {
"ros2": False,
"jsonschema": False,
}
# Check ROS2
try:
import rclpy # type: ignore
import rosidl_runtime_py # type: ignore
dependencies["ros2"] = True
except ImportError:
pass
# Check jsonschema
try:
import jsonschema # type: ignore
dependencies["jsonschema"] = True
except ImportError:
pass
return dependencies

View File

View 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

View 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)

View 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()

View 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),
)

View 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,
# 整数类型都映射到intPython会自动处理范围
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",
# 整数类型都映射到intPython会自动处理范围
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)

View 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
View 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

View File

View File

@@ -0,0 +1,303 @@
from typing import TYPE_CHECKING, Any, Dict, Optional
import jsonschema
from msgcenterpy.core.envelope import MessageEnvelope, create_envelope
from msgcenterpy.core.message_instance import MessageInstance
from msgcenterpy.core.type_converter import TypeConverter
from msgcenterpy.core.type_info import ConstraintType, Consts, TypeInfo
from msgcenterpy.core.types import MessageType
if TYPE_CHECKING:
from msgcenterpy.core.field_accessor import FieldAccessor
class JSONSchemaMessageInstance(MessageInstance[Dict[str, Any]]):
"""JSON Schema消息实例支持类型信息提取和字段访问器"""
_validation_errors: list[str] = []
_json_schema: Dict[str, Any] = dict()
_json_data: Dict[str, Any] = dict()
def __init__(self, inner_data: Dict[str, Any], schema: Dict[str, Any], **kwargs: Any) -> None:
"""
初始化JSON Schema消息实例
Args:
inner_data: JSON数据字典
schema: JSON Schema定义必需
"""
# 直接存储schema和data
self._json_schema = schema
self._json_data = inner_data
self._validation_errors = []
# 验证数据
self._validate_data()
super().__init__(inner_data, MessageType.JSON_SCHEMA)
@property
def json_schema(self) -> Dict[str, Any]:
"""获取JSON Schema"""
return self._json_schema
def _validate_data(self) -> None:
"""根据schema验证数据"""
try:
jsonschema.validate(self._json_data, self._json_schema)
except jsonschema.ValidationError as e:
# 不抛出异常,只记录验证错误
self._validation_errors = [str(e)]
except Exception:
self._validation_errors = ["Schema validation failed"]
else:
self._validation_errors = []
def export_to_envelope(self, **kwargs: Any) -> MessageEnvelope:
"""导出为统一信封字典"""
base_dict = self.get_python_dict()
envelope = create_envelope(
format_name=self.message_type.value,
content=base_dict,
metadata={
"current_format": self.message_type.value,
"source_cls_name": self.__class__.__name__,
"source_cls_module": self.__class__.__module__,
**self._metadata,
},
)
return envelope
@classmethod
def import_from_envelope(cls, data: MessageEnvelope, **kwargs: Any) -> "JSONSchemaMessageInstance":
"""从规范信封创建JSON Schema实例"""
content = data["content"]
properties = data["metadata"]["properties"]
json_schema = properties["json_schema"]
instance = cls(content, json_schema)
return instance
def get_python_dict(self) -> Dict[str, Any]:
"""获取当前所有的字段名和对应的原始值"""
return self._json_data.copy()
def set_python_dict(self, value: Dict[str, Any], **kwargs: Any) -> bool:
"""设置所有字段的值,只做已有字段的更新"""
# 获取根访问器
root_accessor = self._field_accessor
if root_accessor is not None:
root_accessor.update_from_dict(source_data=value)
# 重新验证数据
self._validate_data()
return True
def _get_schema_from_path(self, path: str) -> Dict[str, Any]:
"""根据访问器路径获取对应的JSON Schema定义
Args:
path: 字段访问器的完整路径,如 "MSG_CENTER_ROOT.user.address"
Returns:
对应路径的JSON Schema定义
"""
# 移除根路径前缀
if path.startswith(Consts.ACCESSOR_ROOT_NODE):
if path == Consts.ACCESSOR_ROOT_NODE:
return self._json_schema
path = path[len(Consts.ACCESSOR_ROOT_NODE) + 1 :]
# 如果路径为空返回根schema
if not path:
return self._json_schema
# 分割路径并逐级导航
path_parts = path.split(".")
current_schema = self._json_schema
for part in path_parts:
# 检查当前schema是否有properties
if "properties" not in current_schema:
return {}
properties = current_schema["properties"]
if part not in properties:
return {}
current_schema = properties[part]
# 如果当前schema是数组需要获取items的schema
if current_schema.get("type") == "array" and "items" in current_schema:
current_schema = current_schema["items"]
return current_schema
def _get_property_schema_for_field(self, field_name: str, parent_field_accessor: "FieldAccessor") -> Dict[str, Any]:
"""获取字段的JSON Schema属性定义
Args:
field_name: 字段名
parent_field_accessor: 父级字段访问器
Returns:
字段的JSON Schema属性定义
"""
# 获取父级的schema定义
parent_schema = self._get_schema_from_path(parent_field_accessor.full_path_from_root)
# 从父级schema的properties中获取字段定义
if "properties" in parent_schema:
return parent_schema["properties"].get(field_name, {}) # type: ignore[no-any-return]
elif parent_schema.get("type") == "array" and "items" in parent_schema:
# 如果父级是数组获取items的属性
items_schema = parent_schema["items"]
if "properties" in items_schema:
return items_schema["properties"].get(field_name, {}) # type: ignore[no-any-return]
return {}
# TypeInfoProvider 接口实现
def get_field_type_info(
self, field_name: str, field_value: Any, parent_field_accessor: "FieldAccessor"
) -> Optional[TypeInfo]:
"""从JSON Schema定义中提取字段类型信息"""
# 构建完整路径
full_path = f"{parent_field_accessor.full_path_from_root}.{field_name}"
# 获取字段的JSON Schema定义
property_schema = self._get_property_schema_for_field(field_name, parent_field_accessor)
# 确定类型信息
python_type = type(field_value)
if "type" in property_schema:
json_type = property_schema["type"]
standard_type = TypeConverter.json_schema_type_to_standard(json_type)
else:
# 如果schema中没有类型定义从Python类型推断
standard_type = TypeConverter.python_type_to_standard(python_type)
json_type = TypeConverter.standard_type_to_json_schema_type(standard_type)
# 创建基础TypeInfo
type_info = TypeInfo(
field_name=field_name,
field_path=full_path,
standard_type=standard_type,
python_type=python_type,
original_type=json_type,
current_value=field_value,
)
# 提取约束信息
self._extract_constraints_from_schema(type_info, property_schema)
# 检查字段是否在父级的required列表中
parent_schema = self._get_schema_from_path(parent_field_accessor.full_path_from_root)
required_fields = parent_schema.get("required", [])
if field_name in required_fields:
type_info.add_constraint(ConstraintType.REQUIRED, True, "Field is required by JSON Schema")
# 处理数组类型
if json_type == "array":
type_info.is_array = True
self._extract_array_constraints(type_info, property_schema)
# 处理对象类型
elif json_type == "object":
type_info.is_object = True
self._extract_object_constraints(type_info, property_schema)
# 设置默认值
if "default" in property_schema:
type_info.default_value = property_schema["default"]
return type_info
@classmethod
def _extract_constraints_from_schema(cls, type_info: TypeInfo, property_schema: Dict[str, Any]) -> None:
"""从JSON Schema属性中提取约束条件"""
# 数值约束
if "minimum" in property_schema:
type_info.add_constraint(ConstraintType.MIN_VALUE, property_schema["minimum"])
if "maximum" in property_schema:
type_info.add_constraint(ConstraintType.MAX_VALUE, property_schema["maximum"])
if "exclusiveMinimum" in property_schema:
type_info.add_constraint(ConstraintType.EXCLUSIVE_MIN, property_schema["exclusiveMinimum"])
if "exclusiveMaximum" in property_schema:
type_info.add_constraint(ConstraintType.EXCLUSIVE_MAX, property_schema["exclusiveMaximum"])
if "multipleOf" in property_schema:
type_info.add_constraint(ConstraintType.MULTIPLE_OF, property_schema["multipleOf"])
# 字符串约束
if "minLength" in property_schema:
type_info.add_constraint(ConstraintType.MIN_LENGTH, property_schema["minLength"])
if "maxLength" in property_schema:
type_info.add_constraint(ConstraintType.MAX_LENGTH, property_schema["maxLength"])
if "pattern" in property_schema:
type_info.add_constraint(ConstraintType.PATTERN, property_schema["pattern"])
# 枚举约束
if "enum" in property_schema:
type_info.add_constraint(ConstraintType.ENUM_VALUES, property_schema["enum"])
# 格式约束
if "format" in property_schema:
type_info.add_constraint(ConstraintType.FORMAT, property_schema["format"])
# 默认值
if "default" in property_schema:
type_info.add_constraint(ConstraintType.DEFAULT_VALUE, property_schema["default"])
@classmethod
def _extract_array_constraints(cls, type_info: TypeInfo, property_schema: Dict[str, Any]) -> None:
"""提取数组类型的约束"""
if "minItems" in property_schema:
type_info.add_constraint(ConstraintType.MIN_ITEMS, property_schema["minItems"])
if "maxItems" in property_schema:
type_info.add_constraint(ConstraintType.MAX_ITEMS, property_schema["maxItems"])
if "uniqueItems" in property_schema:
type_info.add_constraint(ConstraintType.UNIQUE_ITEMS, property_schema["uniqueItems"])
# 提取数组元素类型信息
items_schema = property_schema.get("items")
if isinstance(items_schema, dict) and "type" in items_schema:
element_type = TypeConverter.json_schema_type_to_standard(items_schema["type"])
type_info.element_type_info = TypeInfo(
field_name=f"{type_info.field_name}_item",
field_path=f"{type_info.field_path}_item",
standard_type=element_type,
python_type=TypeConverter.standard_to_python_type(element_type),
original_type=items_schema["type"],
current_value=None,
)
# 递归提取元素约束
cls._extract_constraints_from_schema(type_info.element_type_info, items_schema)
@classmethod
def _extract_object_constraints(cls, type_info: TypeInfo, property_schema: Dict[str, Any]) -> None:
"""提取对象类型的约束"""
# 对象类型的属性定义
properties = property_schema.get("properties", {})
required_fields = property_schema.get("required", [])
for prop_name, prop_schema in properties.items():
if isinstance(prop_schema, dict) and "type" in prop_schema:
prop_type = TypeConverter.json_schema_type_to_standard(prop_schema["type"])
prop_type_info = TypeInfo(
field_name=prop_name,
field_path=f"{type_info.field_path}.{prop_name}",
standard_type=prop_type,
python_type=TypeConverter.standard_to_python_type(prop_type),
original_type=prop_schema["type"],
current_value=None,
)
# 递归提取属性约束
cls._extract_constraints_from_schema(prop_type_info, prop_schema)
# 如果字段在required列表中添加REQUIRED约束
if prop_name in required_fields:
prop_type_info.add_constraint(
ConstraintType.REQUIRED,
True,
"Field is required by JSON Schema",
)
type_info.object_fields[prop_name] = prop_type_info

View File

@@ -0,0 +1,242 @@
import array
import importlib
from collections import OrderedDict
from typing import TYPE_CHECKING, Any, Dict, Optional, Type
from rosidl_parser.definition import NamespacedType # type: ignore
from rosidl_runtime_py import ( # type: ignore
import_message_from_namespaced_type,
message_to_ordereddict,
set_message_fields,
)
from msgcenterpy.core.envelope import MessageEnvelope, create_envelope
from msgcenterpy.core.message_instance import MessageInstance
from msgcenterpy.core.type_converter import TypeConverter
from msgcenterpy.core.type_info import ConstraintType, Consts, TypeInfo
from msgcenterpy.core.types import MessageType
if TYPE_CHECKING:
from msgcenterpy.core.field_accessor import FieldAccessor
class ROS2MessageInstance(MessageInstance[Any]):
"""ROS2消息实例支持类型信息提取和字段访问器"""
ros_msg_cls: Type[Any] = None # type: ignore
@classmethod
def get_ros_msg_cls_path(cls, ros_msg_cls: Type[Any]) -> str:
return ros_msg_cls.__module__ + "." + ros_msg_cls.__name__
@property
def ros_msg_cls_path(self) -> str:
return self.get_ros_msg_cls_path(self.ros_msg_cls)
@classmethod
def get_ros_msg_cls_namespace(cls, ros_msg_cls: Type[Any]) -> str:
class_name, module_name = ros_msg_cls.__name__, ros_msg_cls.__module__
package = module_name.split(".")[0] if module_name else ""
interface = (
"msg"
if ".msg" in module_name
else "srv"
if ".srv" in module_name
else "action"
if ".action" in module_name
else "msg"
)
return f"{package}/{interface}/{class_name}" if package and class_name else f"{module_name}.{class_name}"
@property
def ros_msg_cls_namespace(self) -> str:
return self.get_ros_msg_cls_namespace(self.ros_msg_cls)
@classmethod
def obtain_ros_cls_from_str(cls, message_type: str | Type[Any]) -> Type[Any]:
# 需要先解析出正确的消息类
if isinstance(message_type, str):
if "/" in message_type:
namespace, name = message_type.rsplit("/", 1)
message_type = import_message_from_namespaced_type(NamespacedType(namespace.split("/"), name))
elif "." in message_type:
module_path, class_name = message_type.rsplit(".", 1)
mod = importlib.import_module(module_path)
message_type = getattr(mod, class_name)
return message_type # type: ignore
def __init__(self, inner_data: Any, **kwargs: Any) -> None:
self.ros_msg_cls = inner_data.__class__
if not isinstance(self.ros_msg_cls, type):
raise TypeError(f"Expected ROS message class to be a type, got {type(self.ros_msg_cls)}")
super().__init__(inner_data, MessageType.ROS2)
def export_to_envelope(self, **kwargs: Any) -> MessageEnvelope:
"""导出为统一信封字典
用户可从 metadata.properties 中读取:
- properties.ros_msg_cls_namespace
- properties.ros_msg_cls_path
"""
base_dict = self.get_python_dict()
export_envelope = create_envelope(
format_name=self.message_type.value,
content=base_dict,
metadata={
"current_format": self.message_type.value,
"source_cls_name": self.inner_data.__class__.__name__,
"source_cls_module": self.inner_data.__class__.__module__,
**self._metadata,
},
)
return export_envelope
@classmethod
def _ordered_to_dict(cls, obj: Any) -> Any:
if isinstance(obj, OrderedDict):
return {k: cls._ordered_to_dict(v) for k, v in obj.items()}
elif isinstance(obj, tuple):
return tuple(cls._ordered_to_dict(v) for v in obj)
elif isinstance(obj, (list, array.array)):
return [cls._ordered_to_dict(v) for v in obj]
else:
return obj
@classmethod
def import_from_envelope(cls, data: MessageEnvelope, **kwargs: Any) -> "ROS2MessageInstance":
"""从规范信封创建ROS2实例仅 data 一个参数)。
类型信息从 data.metadata.properties 读取
"""
content = data["content"]
properties = data["metadata"]["properties"]
ros_msg_cls = cls.obtain_ros_cls_from_str(properties["ros_msg_cls_namespace"]) or cls.obtain_ros_cls_from_str(
properties["ros_msg_cls_path"]
)
if ros_msg_cls is None:
raise ValueError(
"ros2 type must be provided via metadata.properties.ros_msg_cls_namespace or legacy type_info.ros_namespaced"
)
ros_msg = ros_msg_cls()
set_message_fields(ros_msg, content)
instance = ROS2MessageInstance(ros_msg)
return instance
def get_python_dict(self) -> Dict[str, Any]:
"""获取当前所有的字段名和对应的原始值,使用 SLOT_TYPES 进行类型推断和嵌套导入"""
base_obj = message_to_ordereddict(self.inner_data)
base_dict = self._ordered_to_dict(base_obj)
return base_dict # type: ignore[no-any-return]
def set_python_dict(self, value: Dict[str, Any], **kwargs: Any) -> bool:
"""获取当前所有的字段名和对应的原始值,使用 SLOT_TYPES 进行类型推断和嵌套导入"""
timestamp_fields = set_message_fields(self.inner_data, value, **kwargs)
# todo: 因为ROS自身机制字段并不会增减所以需要更新cache中所有accessor的值通过parent获取
return True
# TypeInfoProvider 接口实现
def get_field_type_info(
self, field_name: str, field_value: Any, parent_field_accessor: "FieldAccessor"
) -> Optional[TypeInfo]:
"""从ROS2消息定义中提取字段类型信息
使用 ROS 消息的 SLOT_TYPES 获取精确的类型信息,并通过 TypeConverter 转换为标准类型
"""
# 通过 parent_field_accessor 获取 ROS 消息实例
ros_msg_instance = parent_field_accessor.value
# 构建完整路径用于TypeInfo
full_path = f"{parent_field_accessor.full_path_from_root}.{field_name}"
# noinspection PyProtectedMember
slots = ros_msg_instance._fields_and_field_types
slot_types = ros_msg_instance.SLOT_TYPES
# 通过 zip 找到 field_name 对应的类型定义
ros_definition_type = None
for slot_name, slot_type in zip(slots, slot_types):
if slot_name == field_name:
ros_definition_type = slot_type
break
if ros_definition_type is None:
raise ValueError(f"Field '{field_name}' not found in ROS message slots")
# 使用 TypeConverter 转换为标准类型
standard_type = TypeConverter.rosidl_definition_to_standard(ros_definition_type)
# 创建 TypeInfo
type_info = TypeInfo(
field_name=field_name,
field_path=full_path,
standard_type=standard_type,
python_type=type(field_value),
original_type=ros_definition_type,
)
type_info.current_value = field_value
# 从 rosidl 定义中提取详细类型信息(约束、数组信息等)
self._extract_from_rosidl_definition(type_info)
return type_info
def _extract_from_rosidl_definition(self, type_info: TypeInfo) -> None:
"""从rosidl_parser定义中提取详细类型信息
Args:
type_info: 要填充的TypeInfo对象
"""
from rosidl_parser.definition import (
AbstractNestedType,
Array,
BasicType,
BoundedSequence,
BoundedString,
BoundedWString,
NamespacedType,
UnboundedSequence,
)
# 从type_info获取所需信息
definition_type = type_info.original_type
get_element_type = False
# 提取约束信息
if isinstance(definition_type, (BoundedString, BoundedWString)):
type_info.add_constraint(ConstraintType.MAX_LENGTH, definition_type.maximum_size)
elif isinstance(definition_type, Array):
type_info.is_array = True
type_info.array_size = definition_type.size
type_info.add_constraint(ConstraintType.MIN_ITEMS, definition_type.size)
type_info.add_constraint(ConstraintType.MAX_ITEMS, definition_type.size)
get_element_type = True
elif isinstance(definition_type, BoundedSequence):
type_info.is_array = True
type_info.add_constraint(ConstraintType.MAX_ITEMS, definition_type.maximum_size)
get_element_type = True
elif isinstance(definition_type, UnboundedSequence):
type_info.is_array = True
get_element_type = True
elif isinstance(definition_type, BasicType):
# 基础类型的约束将在 field_accessor 中自动添加
pass
elif isinstance(definition_type, NamespacedType):
# 对象类型,标记为对象并提取字段信息
type_info.is_object = True
type_info.add_constraint(ConstraintType.TYPE_KEEP, True)
# 这里可以进一步扩展来提取对象字段信息
# 提取元素类型信息
if get_element_type:
if not isinstance(definition_type, AbstractNestedType):
raise TypeError(f"Expected AbstractNestedType for element type extraction, got {type(definition_type)}")
# 创建元素类型的TypeInfo并递归填充
std_type = TypeConverter.rosidl_definition_to_standard(definition_type.value_type)
python_type = TypeConverter.standard_to_python_type(std_type)
type_info.element_type_info = TypeInfo(
field_name=Consts.ELEMENT_TYPE_INFO_SYMBOL,
field_path=Consts.ELEMENT_TYPE_INFO_SYMBOL,
standard_type=std_type,
python_type=python_type,
original_type=definition_type.value_type,
)
self._extract_from_rosidl_definition(type_info.element_type_info)

View File

View File

@@ -0,0 +1,29 @@
import functools
import warnings
from typing import Any, Callable
def experimental(
reason: str = "This API is experimental and may change or be removed in future.",
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""
装饰器:标记函数为实验性。
调用时会发出 RuntimeWarning。
:param reason: 警告信息,可以自定义说明原因
"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
warnings.warn(
f"Call to experimental function '{func.__name__}': {reason}",
category=RuntimeWarning,
stacklevel=2,
)
return func(*args, **kwargs)
wrapper.__experimental__ = True # type: ignore[attr-defined] # 给函数打个标记,方便外部检测
return wrapper
return decorator