diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 1150194..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[bumpversion] -current_version = 0.1.5 -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}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83ead25..b894f4e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst index c13412d..c0ad03a 100644 --- a/docs/development/contributing.rst +++ b/docs/development/contributing.rst @@ -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 `_ 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: diff --git a/msgcenterpy/instances/json_schema_instance.py b/msgcenterpy/instances/json_schema_instance.py index 5b4c4cd..d100520 100644 --- a/msgcenterpy/instances/json_schema_instance.py +++ b/msgcenterpy/instances/json_schema_instance.py @@ -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消息实例 diff --git a/pyproject.toml b/pyproject.toml index a0ec8c3..89e5986 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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,32 @@ 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.5" +commit = true +tag = 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}"' diff --git a/tests/test_json_schema_instance.py b/tests/test_json_schema_instance.py index 5bad6ae..6231d49 100644 --- a/tests/test_json_schema_instance.py +++ b/tests/test_json_schema_instance.py @@ -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 相关测试"""