mirror of
https://github.com/ZGCA-Forge/MsgCenterPy.git
synced 2026-03-24 09:39:16 +00:00
Compare commits
24 Commits
v0.1.3
...
182c5187a0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
182c5187a0 | ||
|
|
9e6007eb60 | ||
|
|
03b3b1144c | ||
|
|
97c76708cd | ||
|
|
05d33086a9 | ||
|
|
34fa239d19 | ||
|
|
37010bdeb1 | ||
|
|
c1e65915c2 | ||
|
|
64963e02d9 | ||
|
|
588b2cffbd | ||
|
|
a93f926448 | ||
|
|
67e1c43a4a | ||
|
|
083c4b0cc2 | ||
|
|
e5b99ca134 | ||
|
|
31c89ccc26 | ||
|
|
58628910f4 | ||
|
|
4153d99344 | ||
|
|
3d0cdcd423 | ||
|
|
419ed15d70 | ||
|
|
5262e9bfd5 | ||
|
|
2d5fd8b74e | ||
|
|
043f934a42 | ||
|
|
0faaa9ebdb | ||
|
|
c4d55fed1b |
@@ -1,10 +0,0 @@
|
||||
[bumpversion]
|
||||
current_version = 0.1.3
|
||||
commit = True
|
||||
tag = True
|
||||
tag_name = v{new_version}
|
||||
message = Bump version: {current_version} → {new_version}
|
||||
|
||||
[bumpversion:file:msgcenterpy/__init__.py]
|
||||
search = __version__ = "{current_version}"
|
||||
replace = __version__ = "{new_version}"
|
||||
49
.github/workflows/ci.yml
vendored
49
.github/workflows/ci.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10" # Use minimum version for consistency
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -33,27 +33,27 @@ jobs:
|
||||
with:
|
||||
extra_args: --all-files
|
||||
|
||||
# Step 2: Basic build and test with minimum Python version (3.10)
|
||||
# Step 2: Basic build and test
|
||||
basic-build:
|
||||
name: Basic build (Python 3.10, Ubuntu)
|
||||
name: Basic build (Python 3.11, Ubuntu)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [code-format] # Only run after code formatting passes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python 3.10
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Cache pip dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ubuntu-pip-3.10-${{ hashFiles('**/pyproject.toml') }}
|
||||
key: ubuntu-pip-3.11-${{ hashFiles('**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
ubuntu-pip-3.10-
|
||||
ubuntu-pip-3.11-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
needs: [basic-build] # Only run after basic build passes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Miniconda
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
needs: [basic-build] # Run in parallel with ROS2 test after basic build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Run Safety CLI to check for vulnerabilities
|
||||
uses: pyupio/safety-action@v1
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
args: --detailed-output --output-format json
|
||||
|
||||
- name: Upload security reports
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: security-reports
|
||||
path: |
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
needs: [basic-build] # Run in parallel with other checks
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
run: twine check dist/*
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
@@ -173,10 +173,10 @@ jobs:
|
||||
exclude:
|
||||
# Skip the combination we already tested in basic-build
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.10"
|
||||
python-version: "3.11"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache pip dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }}
|
||||
@@ -195,20 +195,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest
|
||||
python -m pip install pytest
|
||||
pip install -e .[dev]
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-line-length=200 --extend-ignore=E203,W503,F401,E402,E721,F841 --statistics
|
||||
|
||||
- name: Type checking with mypy
|
||||
run: |
|
||||
mypy msgcenterpy --disable-error-code=unused-ignore
|
||||
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||
|
||||
|
||||
56
.github/workflows/publish.yml
vendored
56
.github/workflows/publish.yml
vendored
@@ -29,12 +29,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10" # Use minimum version for consistency
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -46,27 +46,27 @@ jobs:
|
||||
with:
|
||||
extra_args: --all-files
|
||||
|
||||
# Step 2: Basic build and test with minimum Python version (3.10)
|
||||
# Step 2: Basic build and test
|
||||
basic-build:
|
||||
name: Basic build (Python 3.10, Ubuntu)
|
||||
name: Basic build (Python 3.11, Ubuntu)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [code-format] # Only run after code formatting passes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python 3.10
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Cache pip dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ubuntu-pip-3.10-${{ hashFiles('**/pyproject.toml') }}
|
||||
key: ubuntu-pip-3.11-${{ hashFiles('**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
ubuntu-pip-3.10-
|
||||
ubuntu-pip-3.11-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -78,14 +78,14 @@ jobs:
|
||||
run: |
|
||||
pytest -v
|
||||
|
||||
# Step 3: ROS2 integration test
|
||||
# Step 3: ROS2 integration test
|
||||
test-with-ros2:
|
||||
name: ROS2 integration test
|
||||
runs-on: ubuntu-latest
|
||||
needs: [basic-build] # Only run after basic build passes
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Miniconda
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
@@ -125,23 +125,17 @@ jobs:
|
||||
needs: [basic-build] # Run in parallel with ROS2 test after basic build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
- name: Run Safety CLI to check for vulnerabilities
|
||||
uses: pyupio/safety-action@v1
|
||||
with:
|
||||
python-version: "3.10" # Use minimum version for consistency
|
||||
|
||||
- name: Install security tools
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install "safety>=3.0.0" "typer<0.12.0" "marshmallow<4.0.0"
|
||||
|
||||
- name: Run safety security scan
|
||||
run: safety check --output json > safety-report.json
|
||||
api-key: ${{ secrets.SAFETY_CHECK }}
|
||||
output-format: json
|
||||
args: --detailed-output --output-format json
|
||||
|
||||
- name: Upload security reports
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: security-reports
|
||||
path: |
|
||||
@@ -154,7 +148,7 @@ jobs:
|
||||
needs: [test-with-ros2]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
@@ -203,7 +197,7 @@ jobs:
|
||||
twine check dist/*
|
||||
|
||||
- name: Upload distributions
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
@@ -223,7 +217,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Retrieve release distributions
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
@@ -246,7 +240,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Retrieve release distributions
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
@@ -266,7 +260,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Retrieve release distributions
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
@@ -285,7 +279,7 @@ jobs:
|
||||
if: always() && (needs.pypi-publish.result == 'success' || needs.test-pypi-publish.result == 'success')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Create deployment summary
|
||||
run: |
|
||||
|
||||
@@ -1,81 +1,77 @@
|
||||
# MsgCenterPy pre-commit hooks
|
||||
# All tool configs are centralised in pyproject.toml
|
||||
|
||||
repos:
|
||||
# Code formatting
|
||||
# ── Code formatting ───────────────────────────────────────────
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.12.1
|
||||
rev: 26.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
args: ["--line-length=120"]
|
||||
# Reads [tool.black] from pyproject.toml
|
||||
|
||||
# Import sorting
|
||||
# ── Import sorting ────────────────────────────────────────────
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black", "--multi-line", "3"]
|
||||
# Reads [tool.isort] from pyproject.toml
|
||||
|
||||
# Linting
|
||||
# ── Linting ───────────────────────────────────────────────────
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.0.0
|
||||
rev: 7.3.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args:
|
||||
- "--max-line-length=200" # Allow longer lines after black formatting
|
||||
- "--extend-ignore=E203,W503,F401,E402,E721,F841"
|
||||
- "--exclude=build,dist,__pycache__,.mypy_cache,.pytest_cache,htmlcov,.idea,.vscode,docs/_build,msgcenterpy.egg-info"
|
||||
additional_dependencies: ["Flake8-pyproject"]
|
||||
# Reads [tool.flake8] from pyproject.toml via Flake8-pyproject
|
||||
|
||||
# Type checking
|
||||
# ── Type checking ─────────────────────────────────────────────
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
rev: v1.19.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-PyYAML, types-jsonschema, pydantic]
|
||||
args: ["--ignore-missing-imports", "--disable-error-code=unused-ignore"]
|
||||
files: "^(msgcenterpy/)" # Check both source code and tests
|
||||
args: ["--config-file=pyproject.toml"]
|
||||
files: "^(msgcenterpy/)"
|
||||
additional_dependencies:
|
||||
- "pydantic>=2.0"
|
||||
- "types-PyYAML"
|
||||
- "types-jsonschema"
|
||||
|
||||
# General pre-commit hooks
|
||||
# ── Basic file checks ────────────────────────────────────────
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
# File checks
|
||||
- id: trailing-whitespace
|
||||
args: [--markdown-linebreak-ext=md]
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-xml
|
||||
|
||||
# Security
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
- id: check-symlinks
|
||||
- id: check-added-large-files
|
||||
args: ["--maxkb=1000"]
|
||||
|
||||
# Python specific
|
||||
- id: check-ast
|
||||
- id: debug-statements
|
||||
- id: name-tests-test
|
||||
args: ["--django"]
|
||||
args: ["--pytest-test-first"]
|
||||
|
||||
# Security scanning
|
||||
# ── Security scanning ────────────────────────────────────────
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.5
|
||||
rev: 1.9.3
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: ["-c", "pyproject.toml"]
|
||||
additional_dependencies: ["bandit[toml]", "pbr"]
|
||||
exclude: "^tests/"
|
||||
exclude: "^(tests/)"
|
||||
|
||||
# YAML/JSON formatting
|
||||
# ── YAML/JSON/Markdown formatting ────────────────────────────
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [yaml, json, markdown]
|
||||
exclude: "^(build/|dist/|__pycache__/|\\.mypy_cache/|\\.pytest_cache/|htmlcov/|\\.idea/|\\.vscode/|docs/_build/|msgcenterpy\\.egg-info/)"
|
||||
exclude: "^(build/|dist/|__pycache__/|\\.mypy_cache/|\\.pytest_cache/|htmlcov/|\\.vscode/|docs/_build/|msgcenterpy\\.egg-info/)"
|
||||
|
||||
# Global settings
|
||||
default_stages: [pre-commit, pre-push]
|
||||
default_stages: [pre-commit]
|
||||
fail_fast: false
|
||||
|
||||
@@ -88,18 +88,18 @@ Testing
|
||||
Version Management
|
||||
------------------
|
||||
|
||||
This project uses `bump2version` for version management. It's automatically installed with dev dependencies.
|
||||
This project uses `bump-my-version <https://github.com/callowayproject/bump-my-version>`_ for version management. It's automatically installed with dev dependencies. Configuration lives in ``pyproject.toml`` under ``[tool.bumpversion]``.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Bug fixes (0.0.1 → 0.0.2)
|
||||
bump2version patch
|
||||
bump-my-version bump patch
|
||||
|
||||
# New features (0.0.2 → 0.1.0)
|
||||
bump2version minor
|
||||
bump-my-version bump minor
|
||||
|
||||
# Breaking changes (0.1.0 → 1.0.0)
|
||||
bump2version major
|
||||
bump-my-version bump major
|
||||
|
||||
After bumping version, push changes and tags:
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ A multi-format message conversion system supporting seamless conversion
|
||||
between ROS2, Pydantic, Dataclass, JSON, Dict, YAML and JSON Schema.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.3"
|
||||
__version__ = "0.1.8"
|
||||
__license__ = "Apache-2.0"
|
||||
|
||||
from msgcenterpy.core.envelope import MessageEnvelope, create_envelope
|
||||
|
||||
@@ -9,6 +9,8 @@ class Properties(TypedDict, total=False):
|
||||
ros_msg_cls_path: str
|
||||
ros_msg_cls_namespace: str
|
||||
json_schema: Dict[str, Any]
|
||||
typed_dict_class_module: str
|
||||
typed_dict_class_name: str
|
||||
|
||||
|
||||
class FormatMetadata(TypedDict, total=False):
|
||||
|
||||
@@ -358,14 +358,14 @@ class TypeInfoProvider(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def get_field_type_info(
|
||||
self, field_name: str, field_value: Any, field_accessor: "FieldAccessor"
|
||||
self, field_name: str, field_value: Any, parent_field_accessor: "FieldAccessor"
|
||||
) -> Optional[TypeInfo]:
|
||||
"""获取指定字段的类型信息
|
||||
|
||||
Args:
|
||||
field_name: 字段名,简单字段名如 'field'
|
||||
field_value: 字段的当前值,用于动态类型推断,不能为None
|
||||
field_accessor: 字段访问器,提供额外的上下文信息,不能为None
|
||||
parent_field_accessor: 父字段访问器,提供额外的上下文信息,不能为None
|
||||
|
||||
Returns:
|
||||
字段的TypeInfo,如果字段不存在则返回None
|
||||
|
||||
@@ -277,9 +277,16 @@ class TypeInfo:
|
||||
# Special handling for object types
|
||||
if self.is_object and self.object_fields:
|
||||
properties = {}
|
||||
required_fields = []
|
||||
for field_name, field_info in self.object_fields.items():
|
||||
properties[field_name] = field_info.to_json_schema_property(include_constraints)
|
||||
if field_info.has_constraint(ConstraintType.REQUIRED):
|
||||
required_fields.append(field_name)
|
||||
property_schema["properties"] = properties
|
||||
if required_fields:
|
||||
property_schema["required"] = required_fields
|
||||
if self.field_name and self.field_name != Consts.ELEMENT_TYPE_INFO_SYMBOL:
|
||||
property_schema["title"] = self.field_name
|
||||
|
||||
# Add description
|
||||
if self.original_type:
|
||||
|
||||
@@ -11,6 +11,7 @@ class MessageType(Enum):
|
||||
JSON_SCHEMA = "json_schema"
|
||||
DICT = "dict"
|
||||
YAML = "yaml"
|
||||
TYPED_DICT = "typed_dict" # Experimental
|
||||
|
||||
|
||||
class ConversionError(Exception):
|
||||
|
||||
@@ -19,6 +19,87 @@ class JSONSchemaMessageInstance(MessageInstance[Dict[str, Any]]):
|
||||
_json_schema: Dict[str, Any] = dict()
|
||||
_json_data: Dict[str, Any] = dict()
|
||||
|
||||
_JSON_TYPE_DEFAULTS: Dict[str, Any] = {
|
||||
"string": "",
|
||||
"integer": 0,
|
||||
"number": 0.0,
|
||||
"boolean": False,
|
||||
"null": None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def generate_default_from_schema(cls, schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""根据JSON Schema生成包含所有字段默认值的数据字典
|
||||
|
||||
优先使用schema中显式声明的default,否则根据类型生成零值。
|
||||
支持嵌套object递归生成。
|
||||
|
||||
Args:
|
||||
schema: JSON Schema定义(根schema或子object schema)
|
||||
|
||||
Returns:
|
||||
包含所有字段默认值的数据字典
|
||||
|
||||
Examples:
|
||||
>>> schema = {
|
||||
... "type": "object",
|
||||
... "properties": {
|
||||
... "name": {"type": "string"},
|
||||
... "age": {"type": "integer", "default": 18},
|
||||
... "active": {"type": "boolean"},
|
||||
... }
|
||||
... }
|
||||
>>> JSONSchemaMessageInstance.generate_default_from_schema(schema)
|
||||
{'name': '', 'age': 18, 'active': False}
|
||||
"""
|
||||
properties = schema.get("properties", {})
|
||||
result: Dict[str, Any] = {}
|
||||
|
||||
for field_name, field_schema in properties.items():
|
||||
if isinstance(field_schema, dict):
|
||||
result[field_name] = cls._generate_field_default(field_schema)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _generate_field_default(cls, field_schema: Dict[str, Any]) -> Any:
|
||||
"""根据单个字段的schema生成默认值
|
||||
|
||||
Args:
|
||||
field_schema: 字段的JSON Schema定义
|
||||
|
||||
Returns:
|
||||
字段的默认值
|
||||
"""
|
||||
if "default" in field_schema:
|
||||
return field_schema["default"]
|
||||
|
||||
if "const" in field_schema:
|
||||
return field_schema["const"]
|
||||
|
||||
if "enum" in field_schema:
|
||||
enum_values = field_schema["enum"]
|
||||
if enum_values:
|
||||
return enum_values[0]
|
||||
|
||||
json_type = field_schema.get("type")
|
||||
|
||||
if json_type is None:
|
||||
return None
|
||||
|
||||
# 联合类型取第一个非null类型
|
||||
if isinstance(json_type, list):
|
||||
non_null = [t for t in json_type if t != "null"]
|
||||
json_type = non_null[0] if non_null else "null"
|
||||
|
||||
if json_type == "object":
|
||||
return cls.generate_default_from_schema(field_schema)
|
||||
|
||||
if json_type == "array":
|
||||
return []
|
||||
|
||||
return cls._JSON_TYPE_DEFAULTS.get(json_type)
|
||||
|
||||
def __init__(self, inner_data: Dict[str, Any], schema: Dict[str, Any], **kwargs: Any) -> None:
|
||||
"""
|
||||
初始化JSON Schema消息实例
|
||||
|
||||
@@ -40,11 +40,7 @@ class ROS2MessageInstance(MessageInstance[Any]):
|
||||
interface = (
|
||||
"msg"
|
||||
if ".msg" in module_name
|
||||
else "srv"
|
||||
if ".srv" in module_name
|
||||
else "action"
|
||||
if ".action" in module_name
|
||||
else "msg"
|
||||
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}"
|
||||
|
||||
@@ -221,10 +217,9 @@ class ROS2MessageInstance(MessageInstance[Any]):
|
||||
# 基础类型的约束将在 field_accessor 中自动添加
|
||||
pass
|
||||
elif isinstance(definition_type, NamespacedType):
|
||||
# 对象类型,标记为对象并提取字段信息
|
||||
type_info.is_object = True
|
||||
type_info.add_constraint(ConstraintType.TYPE_KEEP, True)
|
||||
# 这里可以进一步扩展来提取对象字段信息
|
||||
self._extract_namespaced_type_fields(type_info, definition_type)
|
||||
# 提取元素类型信息
|
||||
if get_element_type:
|
||||
if not isinstance(definition_type, AbstractNestedType):
|
||||
@@ -240,3 +235,33 @@ class ROS2MessageInstance(MessageInstance[Any]):
|
||||
original_type=definition_type.value_type,
|
||||
)
|
||||
self._extract_from_rosidl_definition(type_info.element_type_info)
|
||||
|
||||
def _extract_namespaced_type_fields(self, type_info: TypeInfo, namespaced_type: "NamespacedType") -> None:
|
||||
"""从 NamespacedType(嵌套 ROS2 消息)中提取所有字段信息,填充 object_fields
|
||||
|
||||
递归处理嵌套的消息类型,确保多层嵌套的结构也能正确提取。
|
||||
|
||||
Args:
|
||||
type_info: 要填充 object_fields 的 TypeInfo 对象
|
||||
namespaced_type: rosidl NamespacedType 定义
|
||||
"""
|
||||
msg_cls = import_message_from_namespaced_type(namespaced_type)
|
||||
msg_instance = msg_cls()
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
slots = msg_instance._fields_and_field_types
|
||||
slot_types = msg_instance.SLOT_TYPES
|
||||
|
||||
for field_name, slot_type in zip(slots, slot_types):
|
||||
std_type = TypeConverter.rosidl_definition_to_standard(slot_type)
|
||||
python_type = TypeConverter.standard_to_python_type(std_type)
|
||||
field_type_info = TypeInfo(
|
||||
field_name=field_name,
|
||||
field_path=f"{type_info.field_path}.{field_name}",
|
||||
standard_type=std_type,
|
||||
python_type=python_type,
|
||||
original_type=slot_type,
|
||||
)
|
||||
self._extract_from_rosidl_definition(field_type_info)
|
||||
field_type_info.add_constraint(ConstraintType.REQUIRED, True)
|
||||
type_info.object_fields[field_name] = field_type_info
|
||||
|
||||
375
msgcenterpy/instances/typed_dict_instance.py
Normal file
375
msgcenterpy/instances/typed_dict_instance.py
Normal file
@@ -0,0 +1,375 @@
|
||||
"""
|
||||
TypedDict Message Instance - Experimental
|
||||
|
||||
This module provides support for TypedDict message instances with type information
|
||||
extraction and field access capabilities.
|
||||
|
||||
WARNING: This implementation is EXPERIMENTAL and may change in future versions.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Type, get_type_hints
|
||||
|
||||
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 TypedDictMessageInstance(MessageInstance[Dict[str, Any]]):
|
||||
"""TypedDict消息实例,支持类型信息提取和字段访问器(实验性)
|
||||
|
||||
EXPERIMENTAL: This class is experimental and may change in future versions.
|
||||
|
||||
Attributes:
|
||||
_typed_dict_class: TypedDict类型定义
|
||||
_typed_dict_data: 实际的字典数据
|
||||
_pydantic_model: 缓存的Pydantic模型(懒加载)
|
||||
_json_schema: 缓存的JSON Schema(懒加载)
|
||||
"""
|
||||
|
||||
_typed_dict_class: Type[Any]
|
||||
_typed_dict_data: Dict[str, Any]
|
||||
_pydantic_model: Optional[Any] = None
|
||||
_json_schema: Optional[Dict[str, Any]] = None
|
||||
|
||||
def __init__(self, inner_data: Dict[str, Any], typed_dict: Type[Any], **kwargs: Any) -> None:
|
||||
"""
|
||||
初始化TypedDict消息实例
|
||||
|
||||
Args:
|
||||
inner_data: 字典数据
|
||||
typed_dict: TypedDict类型定义
|
||||
**kwargs: 额外的关键字参数
|
||||
"""
|
||||
# 发出实验性警告
|
||||
warnings.warn(
|
||||
"TypedDictMessageInstance is experimental and may change in future versions",
|
||||
FutureWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# 验证typed_dict是否为TypedDict类型
|
||||
if not self._is_typed_dict(typed_dict):
|
||||
raise TypeError(f"Expected a TypedDict class, got {type(typed_dict)}")
|
||||
|
||||
self._typed_dict_class = typed_dict
|
||||
self._typed_dict_data = inner_data
|
||||
self._pydantic_model = None
|
||||
self._json_schema = None
|
||||
|
||||
super().__init__(inner_data, MessageType.TYPED_DICT)
|
||||
|
||||
@staticmethod
|
||||
def _is_typed_dict(typed_dict_class: Type[Any]) -> bool:
|
||||
"""检查给定的类是否为TypedDict"""
|
||||
try:
|
||||
# TypedDict类会有__annotations__和__total__等特殊属性
|
||||
return (
|
||||
hasattr(typed_dict_class, "__annotations__")
|
||||
and hasattr(typed_dict_class, "__total__")
|
||||
and hasattr(typed_dict_class, "__required_keys__")
|
||||
and hasattr(typed_dict_class, "__optional_keys__")
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@property
|
||||
def typed_dict_class(self) -> Type[Any]:
|
||||
"""获取TypedDict类型定义"""
|
||||
return self._typed_dict_class
|
||||
|
||||
@property
|
||||
def typed_dict_class_module(self) -> str:
|
||||
"""获取TypedDict类的模块路径"""
|
||||
return self._typed_dict_class.__module__
|
||||
|
||||
@property
|
||||
def typed_dict_class_name(self) -> str:
|
||||
"""获取TypedDict类名"""
|
||||
return self._typed_dict_class.__name__
|
||||
|
||||
@classmethod
|
||||
def get_pydantic_model_from_typed_dict(cls, typed_dict: Type[Any]) -> Any:
|
||||
"""从TypedDict类型创建Pydantic模型(类方法版本)
|
||||
|
||||
优先使用TypeAdapter(Pydantic V2),如果不可用则使用create_model_from_typeddict(Pydantic V1)
|
||||
|
||||
Args:
|
||||
typed_dict: TypedDict类型定义
|
||||
|
||||
Returns:
|
||||
Pydantic模型实例
|
||||
|
||||
Raises:
|
||||
ImportError: 如果Pydantic未安装
|
||||
RuntimeError: 如果无法创建Pydantic模型
|
||||
"""
|
||||
# 尝试使用Pydantic V2的TypeAdapter
|
||||
try:
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
adapter = TypeAdapter(typed_dict)
|
||||
return adapter
|
||||
except ImportError:
|
||||
pass # Pydantic V2不可用,尝试V1
|
||||
except Exception as e:
|
||||
# TypeAdapter创建失败,尝试V1方法
|
||||
warnings.warn(f"Failed to create TypeAdapter: {e}, trying V1 approach", RuntimeWarning)
|
||||
|
||||
# 尝试使用Pydantic V1的create_model_from_typeddict
|
||||
try:
|
||||
from pydantic.main import create_model_from_typeddict
|
||||
|
||||
model = create_model_from_typeddict(typed_dict)
|
||||
return model
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"Pydantic is required for get_pydantic_model_from_typed_dict(). "
|
||||
"Please install it with: pip install pydantic"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to create Pydantic model from TypedDict: {e}") from e
|
||||
|
||||
def get_pydantic_model(self) -> Any:
|
||||
"""获取或创建Pydantic模型(实例方法版本)
|
||||
|
||||
优先使用TypeAdapter(Pydantic V2),如果不可用则使用create_model_from_typeddict(Pydantic V1)
|
||||
|
||||
Returns:
|
||||
Pydantic模型实例
|
||||
|
||||
Raises:
|
||||
ImportError: 如果Pydantic未安装
|
||||
RuntimeError: 如果无法创建Pydantic模型
|
||||
"""
|
||||
if self._pydantic_model is not None:
|
||||
return self._pydantic_model
|
||||
|
||||
self._pydantic_model = self.get_pydantic_model_from_typed_dict(self._typed_dict_class)
|
||||
return self._pydantic_model
|
||||
|
||||
@classmethod
|
||||
def get_json_schema_from_typed_dict(cls, typed_dict: Type[Any]) -> Dict[str, Any]:
|
||||
"""从TypedDict类型生成JSON Schema(类方法版本)
|
||||
|
||||
Args:
|
||||
typed_dict: TypedDict类型定义
|
||||
|
||||
Returns:
|
||||
JSON Schema字典
|
||||
|
||||
Raises:
|
||||
ImportError: 如果Pydantic未安装
|
||||
RuntimeError: 如果无法生成JSON Schema
|
||||
"""
|
||||
pydantic_model = cls.get_pydantic_model_from_typed_dict(typed_dict)
|
||||
|
||||
try:
|
||||
schema: Dict[str, Any]
|
||||
# Pydantic V2 API
|
||||
if hasattr(pydantic_model, "json_schema"):
|
||||
# TypeAdapter的情况
|
||||
schema = pydantic_model.json_schema()
|
||||
return schema
|
||||
elif hasattr(pydantic_model, "model_json_schema"):
|
||||
# BaseModel的情况(V2)
|
||||
schema = pydantic_model.model_json_schema()
|
||||
return schema
|
||||
# Pydantic V1 API
|
||||
elif hasattr(pydantic_model, "schema"):
|
||||
schema = pydantic_model.schema()
|
||||
return schema
|
||||
else:
|
||||
raise RuntimeError("Unable to extract JSON schema from Pydantic model")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to generate JSON schema: {e}") from e
|
||||
|
||||
def get_json_schema(self) -> Dict[str, Any]:
|
||||
"""获取JSON Schema(实例方法版本,利用Pydantic转换)
|
||||
|
||||
Returns:
|
||||
JSON Schema字典
|
||||
|
||||
Raises:
|
||||
ImportError: 如果Pydantic未安装
|
||||
RuntimeError: 如果无法生成JSON Schema
|
||||
"""
|
||||
if self._json_schema is not None:
|
||||
return self._json_schema
|
||||
|
||||
self._json_schema = self.get_json_schema_from_typed_dict(self._typed_dict_class)
|
||||
return self._json_schema
|
||||
|
||||
def export_to_envelope(self, **kwargs: Any) -> MessageEnvelope:
|
||||
"""导出为统一信封字典
|
||||
|
||||
将 typed_dict_class_module, typed_dict_class_name 和 json_schema 保存到 properties,
|
||||
确保 import_from_envelope 可以独立重建实例
|
||||
"""
|
||||
base_dict = self.get_python_dict()
|
||||
|
||||
# 尝试获取 json_schema(如果 Pydantic 可用)
|
||||
json_schema = None
|
||||
try:
|
||||
json_schema = self.get_json_schema()
|
||||
except (ImportError, RuntimeError):
|
||||
# Pydantic 不可用或生成失败,继续但不保存 schema
|
||||
pass
|
||||
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
# 将 typed_dict_class_module, typed_dict_class_name 和 json_schema 保存到 properties
|
||||
if "properties" not in envelope["metadata"]:
|
||||
envelope["metadata"]["properties"] = {} # type: ignore[typeddict-item]
|
||||
envelope["metadata"]["properties"]["typed_dict_class_module"] = self.typed_dict_class_module # type: ignore[typeddict-item]
|
||||
envelope["metadata"]["properties"]["typed_dict_class_name"] = self.typed_dict_class_name # type: ignore[typeddict-item]
|
||||
if json_schema is not None:
|
||||
envelope["metadata"]["properties"]["json_schema"] = json_schema # type: ignore[typeddict-item]
|
||||
|
||||
return envelope
|
||||
|
||||
@classmethod
|
||||
def import_from_envelope(cls, data: MessageEnvelope, **kwargs: Any) -> "TypedDictMessageInstance":
|
||||
"""从规范信封创建TypedDict实例
|
||||
|
||||
优先从 envelope.metadata.properties 读取 json_schema,
|
||||
如果没有 json_schema,则尝试从 typed_dict 参数或 typed_dict_class_path 恢复。
|
||||
|
||||
Args:
|
||||
data: 消息信封
|
||||
**kwargs: 可选的'typed_dict'参数
|
||||
|
||||
Returns:
|
||||
TypedDict实例
|
||||
|
||||
Raises:
|
||||
ValueError: 如果无法确定TypedDict类型
|
||||
"""
|
||||
content = data["content"]
|
||||
properties = data.get("metadata", {}).get("properties", {})
|
||||
|
||||
# 优先从 kwargs 获取 typed_dict
|
||||
typed_dict = kwargs.pop("typed_dict", None)
|
||||
|
||||
# 如果没有提供 typed_dict,尝试从 properties 恢复
|
||||
if typed_dict is None:
|
||||
typed_dict_class_module = properties.get("typed_dict_class_module")
|
||||
typed_dict_class_name = properties.get("typed_dict_class_name")
|
||||
|
||||
if typed_dict_class_module and typed_dict_class_name:
|
||||
# 尝试从模块导入 TypedDict
|
||||
try:
|
||||
import importlib
|
||||
|
||||
module = importlib.import_module(typed_dict_class_module)
|
||||
typed_dict = getattr(module, typed_dict_class_name)
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Unable to import TypedDict '{typed_dict_class_name}' from module '{typed_dict_class_module}': {e}. "
|
||||
"Please provide 'typed_dict' parameter explicitly."
|
||||
) from e
|
||||
|
||||
if typed_dict is None:
|
||||
raise ValueError(
|
||||
"Unable to determine TypedDict type. "
|
||||
"Please provide 'typed_dict' parameter or ensure envelope contains valid type information "
|
||||
"(typed_dict_class_module and typed_dict_class_name)."
|
||||
)
|
||||
|
||||
instance = cls(content, typed_dict, **kwargs)
|
||||
return instance
|
||||
|
||||
def get_python_dict(self) -> Dict[str, Any]:
|
||||
"""获取当前所有的字段名和对应的原始值"""
|
||||
return self._typed_dict_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)
|
||||
return True
|
||||
|
||||
# TypeInfoProvider 接口实现
|
||||
def get_field_type_info(
|
||||
self, field_name: str, field_value: Any, parent_field_accessor: "FieldAccessor"
|
||||
) -> Optional[TypeInfo]:
|
||||
"""从TypedDict定义中提取字段类型信息
|
||||
|
||||
Args:
|
||||
field_name: 字段名
|
||||
field_value: 字段值
|
||||
parent_field_accessor: 父级字段访问器
|
||||
|
||||
Returns:
|
||||
字段的类型信息
|
||||
"""
|
||||
# 构建完整路径
|
||||
full_path = f"{parent_field_accessor.full_path_from_root}.{field_name}"
|
||||
|
||||
# 获取TypedDict的类型提示
|
||||
try:
|
||||
type_hints = get_type_hints(self._typed_dict_class)
|
||||
except Exception:
|
||||
type_hints = {}
|
||||
|
||||
# 获取字段的类型注解
|
||||
field_type_annotation = type_hints.get(field_name)
|
||||
|
||||
# 确定类型信息
|
||||
python_type = type(field_value)
|
||||
if field_type_annotation is not None:
|
||||
# 从类型注解推断标准类型
|
||||
standard_type = TypeConverter.python_type_to_standard(field_type_annotation)
|
||||
else:
|
||||
# 如果没有类型注解,从值的类型推断
|
||||
standard_type = TypeConverter.python_type_to_standard(python_type)
|
||||
|
||||
# 创建基础TypeInfo
|
||||
type_info = TypeInfo(
|
||||
field_name=field_name,
|
||||
field_path=full_path,
|
||||
standard_type=standard_type,
|
||||
python_type=python_type,
|
||||
original_type=field_type_annotation if field_type_annotation is not None else python_type,
|
||||
current_value=field_value,
|
||||
)
|
||||
|
||||
# 检查字段是否为必需字段
|
||||
if hasattr(self._typed_dict_class, "__required_keys__"):
|
||||
required_keys = getattr(self._typed_dict_class, "__required_keys__")
|
||||
if field_name in required_keys:
|
||||
type_info.add_constraint(
|
||||
ConstraintType.REQUIRED,
|
||||
True,
|
||||
"Field is required by TypedDict definition",
|
||||
)
|
||||
|
||||
# 处理列表/数组类型
|
||||
if isinstance(field_value, list):
|
||||
type_info.is_array = True
|
||||
# 可以进一步提取元素类型信息,但这需要更复杂的类型解析
|
||||
# 暂时留给后续版本实现
|
||||
|
||||
# 处理字典/对象类型
|
||||
elif isinstance(field_value, dict):
|
||||
type_info.is_object = True
|
||||
# 可以进一步提取对象字段信息,但这需要递归解析
|
||||
# 暂时留给后续版本实现
|
||||
|
||||
return type_info
|
||||
@@ -40,13 +40,17 @@ ros2 = [
|
||||
]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"black>=22.0.0",
|
||||
"isort>=5.0.0",
|
||||
"mypy>=1.0.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"black>=23.0.0",
|
||||
"isort>=5.13.0",
|
||||
"flake8>=7.0.0",
|
||||
"Flake8-pyproject>=1.2.0",
|
||||
"mypy>=1.8.0",
|
||||
"types-jsonschema>=4.0.0",
|
||||
"types-PyYAML>=6.0.0",
|
||||
"pre-commit>=2.20.0",
|
||||
"bump2version>=1.0.0"
|
||||
"bandit[toml]>=1.7.0",
|
||||
"pre-commit>=3.5.0",
|
||||
"bump-my-version>=0.28.0",
|
||||
]
|
||||
docs = [
|
||||
"sphinx>=5.0.0",
|
||||
@@ -70,14 +74,26 @@ include = ["msgcenterpy*"]
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "msgcenterpy.__version__"}
|
||||
|
||||
# ── Formatting ────────────────────────────────────────────────
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = ['py310', 'py311', 'py312']
|
||||
target-version = ["py310", "py311", "py312"]
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 120
|
||||
multi_line_output = 3
|
||||
|
||||
# ── Linting ───────────────────────────────────────────────────
|
||||
|
||||
[tool.flake8]
|
||||
max-line-length = 200
|
||||
extend-ignore = ["E203", "W503", "W291", "W293", "W391", "F401", "E402", "E721", "F841", "F541"]
|
||||
exclude = ["build", "dist", "__pycache__", ".mypy_cache", ".pytest_cache", "htmlcov", ".vscode", "docs/_build", "msgcenterpy.egg-info"]
|
||||
|
||||
# ── Type checking ─────────────────────────────────────────────
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
plugins = ["pydantic.mypy"]
|
||||
@@ -86,8 +102,11 @@ warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
warn_unused_ignores = false
|
||||
strict_equality = true
|
||||
ignore_missing_imports = true
|
||||
|
||||
# ── Testing ───────────────────────────────────────────────────
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
@@ -95,12 +114,33 @@ python_files = "test_*.py"
|
||||
python_classes = "Test*"
|
||||
python_functions = "test_*"
|
||||
addopts = "-v --tb=short --strict-markers --strict-config -ra --color=yes"
|
||||
|
||||
filterwarnings = [
|
||||
"ignore::DeprecationWarning",
|
||||
"ignore::PendingDeprecationWarning"
|
||||
"ignore::PendingDeprecationWarning",
|
||||
]
|
||||
|
||||
# ── Security ──────────────────────────────────────────────────
|
||||
|
||||
[tool.bandit]
|
||||
exclude_dirs = ["tests", "build", "dist"]
|
||||
skips = ["B101", "B601"]
|
||||
|
||||
# ── Version management ────────────────────────────────────────
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "0.1.8"
|
||||
commit = true
|
||||
tag = true
|
||||
push = true
|
||||
tag_name = "v{new_version}"
|
||||
message = "release: bump version {current_version} → {new_version}"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "pyproject.toml"
|
||||
search = 'current_version = "{current_version}"'
|
||||
replace = 'current_version = "{new_version}"'
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "msgcenterpy/__init__.py"
|
||||
search = '__version__ = "{current_version}"'
|
||||
replace = '__version__ = "{new_version}"'
|
||||
|
||||
@@ -432,6 +432,159 @@ class TestJSONSchemaValidation:
|
||||
assert len(invalid_instance._validation_errors) > 0
|
||||
|
||||
|
||||
class TestGenerateDefaultFromSchema:
|
||||
"""generate_default_from_schema 默认值生成测试"""
|
||||
|
||||
def test_basic_types(self):
|
||||
"""测试基础类型的零值生成"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"s": {"type": "string"},
|
||||
"i": {"type": "integer"},
|
||||
"n": {"type": "number"},
|
||||
"b": {"type": "boolean"},
|
||||
"null_field": {"type": "null"},
|
||||
},
|
||||
}
|
||||
result = JSONSchemaMessageInstance.generate_default_from_schema(schema)
|
||||
|
||||
assert result == {"s": "", "i": 0, "n": 0.0, "b": False, "null_field": None}
|
||||
|
||||
def test_explicit_default_takes_priority(self):
|
||||
"""测试显式 default 优先于类型零值"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "default": "anonymous"},
|
||||
"age": {"type": "integer", "default": 18},
|
||||
"score": {"type": "number", "default": 99.5},
|
||||
},
|
||||
}
|
||||
result = JSONSchemaMessageInstance.generate_default_from_schema(schema)
|
||||
|
||||
assert result == {"name": "anonymous", "age": 18, "score": 99.5}
|
||||
|
||||
def test_enum_uses_first_value(self):
|
||||
"""测试枚举类型使用第一个枚举值"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {"type": "string", "enum": ["active", "inactive", "pending"]},
|
||||
},
|
||||
}
|
||||
result = JSONSchemaMessageInstance.generate_default_from_schema(schema)
|
||||
|
||||
assert result == {"status": "active"}
|
||||
|
||||
def test_const_value(self):
|
||||
"""测试 const 关键字"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {"type": "string", "const": "v2"},
|
||||
},
|
||||
}
|
||||
result = JSONSchemaMessageInstance.generate_default_from_schema(schema)
|
||||
|
||||
assert result == {"version": "v2"}
|
||||
|
||||
def test_array_type(self):
|
||||
"""测试数组类型返回空列表"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
}
|
||||
result = JSONSchemaMessageInstance.generate_default_from_schema(schema)
|
||||
|
||||
assert result == {"tags": []}
|
||||
|
||||
def test_nested_object(self):
|
||||
"""测试嵌套对象递归生成"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"address": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {"type": "string"},
|
||||
"zip": {"type": "integer"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
result = JSONSchemaMessageInstance.generate_default_from_schema(schema)
|
||||
|
||||
assert result == {
|
||||
"user": {
|
||||
"name": "",
|
||||
"address": {"city": "", "zip": 0},
|
||||
}
|
||||
}
|
||||
|
||||
def test_union_type(self):
|
||||
"""测试联合类型取第一个非 null 类型"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nullable_str": {"type": ["string", "null"]},
|
||||
"nullable_int": {"type": ["null", "integer"]},
|
||||
},
|
||||
}
|
||||
result = JSONSchemaMessageInstance.generate_default_from_schema(schema)
|
||||
|
||||
assert result == {"nullable_str": "", "nullable_int": 0}
|
||||
|
||||
def test_no_type_field(self):
|
||||
"""测试缺少 type 定义时返回 None"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"unknown": {"description": "no type defined"},
|
||||
},
|
||||
}
|
||||
result = JSONSchemaMessageInstance.generate_default_from_schema(schema)
|
||||
|
||||
assert result == {"unknown": None}
|
||||
|
||||
def test_empty_schema(self):
|
||||
"""测试空 schema 返回空字典"""
|
||||
result = JSONSchemaMessageInstance.generate_default_from_schema({})
|
||||
assert result == {}
|
||||
|
||||
result2 = JSONSchemaMessageInstance.generate_default_from_schema({"type": "object"})
|
||||
assert result2 == {}
|
||||
|
||||
def test_generated_default_creates_valid_instance(self):
|
||||
"""测试生成的默认值可以直接创建有效的实例"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"count": {"type": "integer"},
|
||||
"active": {"type": "boolean"},
|
||||
"items": {"type": "array", "items": {"type": "string"}},
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"properties": {"version": {"type": "integer"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
defaults = JSONSchemaMessageInstance.generate_default_from_schema(schema)
|
||||
instance = JSONSchemaMessageInstance(defaults, schema)
|
||||
|
||||
assert len(instance._validation_errors) == 0
|
||||
assert instance.get_python_dict() == defaults
|
||||
|
||||
|
||||
# 运行测试的便捷函数
|
||||
def run_json_schema_tests():
|
||||
"""运行 JSON Schema 相关测试"""
|
||||
|
||||
Reference in New Issue
Block a user