zerodds-py v1.0 — Spec Coverage

Audit of the vendor spec docs/specs/zerodds-py-1.0.md against crates/py/ code reality.

Source: docs/specs/zerodds-py-1.0.md (vendor spec, draft 2026-05-15).

Implementation:


§1 Architecture

§1.1 Module Layout

Spec: §1.1 — crate layout with Cargo.toml (crate-type cdylib+rlib), pyproject.toml for maturin, src/lib.rs+ffi.rs, python/zerodds/{__init__,idl,cdr,loader}.py, python/tests/, examples/, docs/ sphinx.

Repo: crates/py/Cargo.toml (crate-type = ["cdylib", "rlib"]), crates/py/pyproject.toml, crates/py/src/lib.rs, crates/py/src/ffi.rs, crates/py/python/zerodds/__init__.py, crates/py/python/zerodds/idl.py, crates/py/python/zerodds/cdr.py, crates/py/python/zerodds/loader.py, crates/py/python/tests/test_smoke.py, crates/py/python/tests/test_idl.py, crates/py/examples/{01_bytes_pubsub,02_shape_pubsub,03_idl_struct_cdr}.py, crates/py/docs/{conf,index,api,quickstart,examples}.{py,rst}.

Tests: layout self-verified by pytest discovery (pytest crates/py/python/tests/).

Status: done

§1.2 PyO3 module zerodds._core — 13 PyClasses

One item per PyClass. Mapping from DDS 1.4 §2.2.2.

§1.2.1 DomainParticipantFactory (PyClass)

Spec: §1.2 + §2.1 — PyClass DomainParticipantFactory with instance(), create_participant(domain_id), create_participant_offline(domain_id), create_participant_fast(domain_id).

Repo: crates/py/src/ffi.rs:53 (#[pyclass(name = "DomainParticipantFactory")]), methods in ffi.rs:60-95.

Tests: crates/py/python/tests/test_smoke.py::test_factory_singleton_and_offline_participant, test_pub_sub_roundtrip_live.

Status: done

§1.2.2 DomainParticipant (PyClass)

Spec: §1.2 + §2.2 — PyClass DomainParticipant with domain_id, topics_len, discovered_participants_count, create_{bytes,shape}_topic, create_{publisher,subscriber}, assert_liveliness, ignore_{participant,topic,publication,subscription}, contains_entity, get_discovered_{topics,participants}.

Repo: crates/py/src/ffi.rs:97 (#[pyclass(name = "DomainParticipant")]), methods in ffi.rs:105-215.

Tests: crates/py/python/tests/test_smoke.py::test_factory_singleton_and_offline_participant, test_bytes_topic_and_reader_are_creatable_offline, test_pub_sub_roundtrip_live.

Status: done

§1.2.3 BytesTopic (PyClass)

Spec: §1.2 — PyClass BytesTopic with name, type_name.

Repo: crates/py/src/ffi.rs:219 (#[pyclass(name = "BytesTopic")]), methods in ffi.rs:224-234.

Tests: crates/py/python/tests/test_smoke.py::test_bytes_topic_and_reader_are_creatable_offline.

Status: done

§1.2.4 ShapeTopic (PyClass)

Spec: §1.2 + §2.7 — PyClass ShapeTopic with name, type_name, type name = "ShapeType".

Repo: crates/py/src/ffi.rs:236 (#[pyclass(name = "ShapeTopic")]), methods in ffi.rs:241-255. Type name from crates/dcps/src/interop.rs:91 (TYPE_NAME: &'static str = "ShapeType").

Tests: crates/py/python/tests/test_smoke.py::test_shape_topic_matches_vendor_interop_type_name.

Status: done

§1.2.5 Publisher (PyClass)

Spec: §1.2 + §2.3 — PyClass Publisher with create_bytes_writer(topic), create_shape_writer(topic).

Repo: crates/py/src/ffi.rs:257 (#[pyclass(name = "Publisher")]), methods in ffi.rs:262-279.

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live.

Status: done

§1.2.6 Subscriber (PyClass)

Spec: §1.2 + §2.3 — PyClass Subscriber with create_bytes_reader(topic), create_shape_reader(topic).

Repo: crates/py/src/ffi.rs:281 (#[pyclass(name = "Subscriber")]), methods in ffi.rs:286-303.

Tests: crates/py/python/tests/test_smoke.py::test_bytes_topic_and_reader_are_creatable_offline, test_pub_sub_roundtrip_live.

Status: done

§1.2.7 BytesWriter (PyClass)

Spec: §1.2 + §2.4 — PyClass BytesWriter with write, wait_for_matched_subscription, matched_subscription_count, publication_matched_status, liveliness_lost_status, offered_deadline_missed_status.

Repo: crates/py/src/ffi.rs:309 (#[pyclass(name = "BytesWriter")]), methods in ffi.rs:314-355.

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live.

Status: done

§1.2.8 BytesReader (PyClass)

Spec: §1.2 + §2.5 — PyClass BytesReader with take, wait_for_data, wait_for_matched_publication, matched_publication_count, subscription_matched_status, sample_lost_status, requested_deadline_missed_status.

Repo: crates/py/src/ffi.rs:357 (#[pyclass(name = "BytesReader")]), methods in ffi.rs:362-418.

Tests: crates/py/python/tests/test_smoke.py::test_bytes_topic_and_reader_are_creatable_offline, test_pub_sub_roundtrip_live.

Status: done

§1.2.9 Shape (PyClass / dataclass)

Spec: §1.2 + §2.7 — PyClass Shape with fields color, x, y, shapesize (default 30), __repr__, conversions From<&PyShape> and From<ShapeType> in both directions.

Repo: crates/py/src/ffi.rs:420 (#[pyclass(name = "Shape")]), methods in ffi.rs:437-468. Rust counterpart in crates/dcps/src/interop.rs:61 (pub struct ShapeType).

Tests: crates/py/python/tests/test_smoke.py::test_shape_constructor_and_repr, test_shape_default_shapesize_is_30, crates/py/python/tests/test_idl.py::test_pyshape_byte_roundtrip, test_pyshape_type_name_set_by_decorator.

Status: done

§1.2.10 ShapeWriter (PyClass)

Spec: §1.2 + §2.4 — PyClass ShapeWriter with write(shape), register_instance(shape), dispose(shape), unregister_instance(shape), wait_for_matched_subscription.

Repo: crates/py/src/ffi.rs:475 (#[pyclass(name = "ShapeWriter")]), methods in ffi.rs:480-537.

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live.

Status: done

§1.2.11 ShapeReader (PyClass)

Spec: §1.2 + §2.5 — PyClass ShapeReader with take, wait_for_data, wait_for_matched_publication.

Repo: crates/py/src/ffi.rs:539 (#[pyclass(name = "ShapeReader")]), methods in ffi.rs:544-583.

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live.

Status: done

§1.2.12 GuardCondition (PyClass)

Spec: §1.2 + §2.6 — PyClass GuardCondition with constructor, set_trigger_value(bool), get_trigger_value() -> bool.

Repo: crates/py/src/ffi.rs:585 (#[pyclass(name = "GuardCondition")]), methods in ffi.rs:590-609. Module registration: crates/py/src/ffi.rs:660 (m.add_class::<PyGuardCondition>()).

Tests: crates/py/python/tests/test_conditions.py::test_guard_condition_default_trigger_is_false, test_guard_condition_set_trigger_roundtrip, test_guard_condition_via_outer_namespace. zerodds.GuardCondition is re-exported in the outer namespace (crates/py/python/zerodds/__init__.py:50 + __all__).

Status: done

§1.2.13 WaitSet (PyClass)

Spec: §1.2 + §2.6 — PyClass WaitSet with constructor, attach_guard_condition(gc), wait(timeout_secs) -> int.

Repo: crates/py/src/ffi.rs:611 (#[pyclass(name = "WaitSet")]), methods in ffi.rs:617-642. Module registration: crates/py/src/ffi.rs:661 (m.add_class::<PyWaitSet>()).

Tests: crates/py/python/tests/test_conditions.py::test_waitset_attach_and_wait_raises_timeout, test_waitset_wakes_when_guard_condition_fires, test_waitset_via_outer_namespace. zerodds.WaitSet is re-exported in the outer namespace.

Status: done

§1.3 Python wrapper python/zerodds/

§1.3.1 __init__.py — re-export

Spec: §1.3 — zerodds.__init__ re-exports the _core PyClasses plus the pure-Python modules cdr and idl.

Repo: crates/py/python/zerodds/__init__.py re-exports the full _core surface — all 13 original PyClasses + the 6 new ones from §6 (DataWriterQos, DataReaderQos, DataWriterListener, DataReaderListener, ReadCondition, QueryCondition) + the three bitmask modules (sample_state, view_state, instance_state). Plus IDL type markers from .idl and the §6.1 IdlTopic/IdlWriter/ IdlReader wrappers from .topic. Fallback when _core is missing: _CoreStub with a clear ImportError.

Tests: crates/py/python/tests/test_smoke.py::test_version_exposed, test_conditions.py::test_guard_condition_via_outer_namespace, test_conditions.py::test_waitset_via_outer_namespace — explicitly verify that both are reachable via zerodds.<Name>.

Status: done

§1.3.2 cdr.py — XCDR2-LE codec

Spec: §1.3 + §3.1 — CdrWriter / CdrReader with alignment rules, primitives (bool, i8..64, u8..64, f32/64), string (length-prefix + null-terminator + padding), bytes (length-prefix + raw). Byte-exact to crates/cdr/src/buffer.rs.

Repo: crates/py/python/zerodds/cdr.py (CdrWriter, CdrReader).

Tests: crates/py/python/tests/test_idl.py::test_cdr_primitive_roundtrip, test_cdr_string_alignment_padding, test_cdr_reader_rejects_truncated_string.

Status: done

§1.3.3 idl.py@idl_struct decorator

Spec: §1.3 + §3.2 — @idl_struct(typename=…) decorator, field type markers (Bool, Int8..64, UInt8..64, Float32/64, String, Bytes, Sequence[T], Array[T, N], Optional[T], idl_enum, idl_union, nested @idl_struct), auto-mapping of Python primitives.

Repo: crates/py/python/zerodds/idl.py with _IdlKind class and type constants (Bool, Int8..64, UInt8..64, Float32/64, String, Bytes), _IdlSequence, _IdlArray, _IdlOptional, _IdlEnum, _IdlUnion, _IdlStruct, idl_struct(), idl_union(), is_idl_struct(), type_name_of().

Tests: crates/py/python/tests/test_idl.py::test_idl_struct_requires_dataclass, test_pyshape_byte_roundtrip, test_pyshape_type_name_set_by_decorator, test_sensor_mixed_fields_roundtrip, test_auto_map_python_primitives, test_nested_struct_roundtrip, test_sequence_of_primitives_roundtrip, test_sequence_of_structs_roundtrip, test_array_fixed_count_roundtrip, test_array_wrong_count_rejected, test_optional_present_and_absent, test_enum_roundtrip, test_enum_unknown_value_raises, test_union_case_int_roundtrip, test_union_case_string_roundtrip, test_union_default_branch_used_for_unknown_disc, test_union_without_default_rejects_unknown_disc, test_idl_struct_resolves_pep563_stringified_annotations.

Status: done

§1.3.4 loader.py — pure-ctypes loader

Spec: §1.3 + §1.4 — pure-ctypes loader against libzerodds.{so,dylib,dll} from crates/zerodds-c-api/. Follows zerodds-ffi-loader-1.0 §3.1. Signatures from crates/zerodds-c-api/include/zerodds.h.

Repo: crates/py/python/zerodds/loader.py (420 lines) with Runtime, Writer, Reader. Consumes crates/zerodds-c-api/include/zerodds.h.

Tests: crates/py/python/tests/test_loader_smoke.py with 4 tests (test_loader_resolves_library, test_loader_runtime_create_participant_offline, test_loader_factory_singleton, test_loader_zerodds_error_is_runtime_error_subclass). Tests skip automatically when libzerodds.{so,dylib,dll} is not found; before a CI run, cargo build -p zerodds-c-api must have been run (or ZERODDS_LIB set).

Status: done — tests run green on Linux-Bench-Host (Debian 12, libzerodds.so from ~/zerodds/target/debug/libzerodds.so, ZERODDS_LIB ENV override). On systems without a built artifact the tests skip cleanly.

§1.4 Two-path choice

Spec: §1.4 — two independent paths: PyO3 (performance, RTPS-native) and pure-ctypes (distro package, zero-build).

Repo: PyO3 path: crates/py/src/ffi.rs + python/zerodds/__init__.py. ctypes path: crates/py/python/zerodds/loader.py + crates/zerodds-c-api/.

Tests: PyO3 fully covered (all PyClass tests from §1.2/§2/§3/§6). ctypes path via test_loader_smoke.py (conditional skip; see §1.3.4).

Status: done — both paths verified live on Linux-Bench-Host (PyO3 via maturin + ctypes via libzerodds.so).

§1.5 Layer position

Spec: §1.5 — Layer 6 (PSMs / bindings). Direct dependencies: zerodds-dcps (Layer 4), zerodds-c-api (Layer 6 C-FFI).

Repo: crates/py/Cargo.toml (zerodds-dcps = { path = "../dcps" }). ctypes path: no Cargo dep, only runtime lookup of libzerodds.{so,dylib,dll}.

Tests: indirectly via cargo build -p zerodds-py + all tests (workspace build).

Status: n/a (informative) — architecture statement, not test-bound.

§2 OMG API coverage (DDS 1.4 §2.2.2)

§2.1 DomainParticipantFactory (DDS 1.4 §2.2.2.2.1)

Spec: §2.1 — mapping table: get_instance(), create_participant, create_participant_offline, implicit delete_participant via Python GC.

Repo: crates/py/src/ffi.rs:60 (fn instance() -> Self), ffi.rs:65 (create_participant_offline), ffi.rs:72 (create_participant), ffi.rs:80 (create_participant_fast). Drop behavior inherits from Arc<DomainParticipant> in crates/dcps/src/factory.rs.

Tests: crates/py/python/tests/test_smoke.py::test_factory_singleton_and_offline_participant, test_pub_sub_roundtrip_live.

Status: done

§2.2 DomainParticipant (DDS 1.4 §2.2.2.2.2)

Spec: §2.2 — 12 operations: get_domain_id, create_topic, create_publisher, create_subscriber, assert_liveliness, ignore_{participant,topic,publication,subscription}, contains_entity, get_discovered_{topics,participants}.

Repo: crates/py/src/ffi.rs:105-215 — all 12 operations as PyMethods (domain_id, topics_len, discovered_participants_count, create_bytes_topic, create_shape_topic, create_publisher, create_subscriber, assert_liveliness, ignore_* (4 variants), contains_entity, get_discovered_topics, get_discovered_participants).

Tests: smoke tests from test_smoke.py plus dedicated tests in crates/py/python/tests/test_participant_ignore.py (9 tests): test_ignore_participant_unknown_handle_is_silent, test_ignore_topic_unknown_handle_is_silent, test_ignore_publication_unknown_handle_is_silent, test_ignore_subscription_unknown_handle_is_silent, test_contains_entity_false_for_unknown_handle, test_get_discovered_topics_offline_is_empty, test_get_discovered_participants_offline_is_empty, test_discovered_participants_count_offline_is_zero, test_assert_liveliness_offline_does_not_raise.

Status: done

§2.3 Publisher / Subscriber (DDS 1.4 §2.2.2.4.1 / §2.2.2.5.1)

Spec: §2.3 — Publisher.create_datawriterpub.create_{bytes,shape}_writer(topic), Subscriber.create_datareadersub.create_{bytes,shape}_reader(topic).

Repo: crates/py/src/ffi.rs:264 (Publisher create_bytes_writer), ffi.rs:272 (create_shape_writer), ffi.rs:288 (Subscriber create_bytes_reader), ffi.rs:296 (create_shape_reader).

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live, test_bytes_topic_and_reader_are_creatable_offline.

Status: done

§2.4 DataWriter (DDS 1.4 §2.2.2.4.2)

Spec: §2.4 — mapping table: write, register_instance, unregister_instance, dispose, get_matched_subscriptions, get_publication_matched_status, get_liveliness_lost_status, get_offered_deadline_missed_status plus wait_for_matched_subscription sync helper.

Repo: crates/py/src/ffi.rs:317 (BytesWriter write), ffi.rs:324 (wait_for_matched_subscription), ffi.rs:337-355 (status getters + matched_subscription_count). ShapeWriter in ffi.rs:482 (write), ffi.rs:492 (register_instance), ffi.rs:502 (dispose), ffi.rs:515 (unregister_instance), ffi.rs:525 (wait_for_matched_subscription).

Tests: smoke tests + dedicated lifecycle tests in crates/py/python/tests/test_writer_lifecycle.py (6 tests): test_register_instance_returns_nonzero_handle, test_register_instance_is_idempotent_for_same_key, test_register_different_keys_yield_different_handles, test_dispose_does_not_raise, test_unregister_instance_does_not_raise, test_lifecycle_full_sequence.

Status: done

§2.5 DataReader (DDS 1.4 §2.2.2.5.3)

Spec: §2.5 — take, wait_for_data, wait_for_matched_publication, matched_publication_count, subscription_matched_status, sample_lost_status, requested_deadline_missed_status. read and wait_for_historical_data deliberately omitted.

Repo: crates/py/src/ffi.rs:365 (BytesReader take), ffi.rs:375 (wait_for_data), ffi.rs:381 (wait_for_matched_publication), ffi.rs:394-418 (status getters + matched_publication_count). ShapeReader analogously in ffi.rs:546-583.

Tests: smoke tests + dedicated status-getter tests in crates/py/python/tests/test_status_getters.py (8 tests): test_publication_matched_status_shape, test_subscription_matched_status_shape, test_liveliness_lost_status_shape, test_offered_deadline_missed_status_shape, test_sample_lost_status_shape, test_requested_deadline_missed_status_shape, test_matched_subscription_count_zero_offline, test_matched_publication_count_zero_offline.

Status: done

§2.6 WaitSet / conditions (DDS 1.4 §2.2.2.6)

Spec: §2.6 — WaitSet, GuardCondition with attach_guard_condition, wait, set/get_trigger_value. ReadCondition/QueryCondition are available via §6.6. WaitSet + GuardCondition in v1.0 reachable only directly via zerodds._core, not in the outer namespace.

Repo: crates/py/src/ffi.rs:585-642. Module registration: ffi.rs:660-661.

Tests: crates/py/python/tests/test_conditions.py (GuardCondition + WaitSet, 6 tests, see §1.2.12/§1.2.13) + crates/py/python/tests/test_read_conditions.py (ReadCondition + QueryCondition + bitmask constants, 11 tests, see §6.6). zerodds.GuardCondition, zerodds.WaitSet, zerodds.ReadCondition, zerodds.QueryCondition are all re-exported in the outer namespace.

Status: done

§2.7 ShapeType — cross-vendor interop type

Spec: §2.7 — Shape PyClass byte-identical to ShapeType in crates/dcps/src/interop.rs. Fields color: String, x: i32, y: i32, shapesize: i32 (default 30). Type name on the wire = "ShapeType".

Repo: crates/py/src/ffi.rs:420 (#[pyclass(name = "Shape")]), constructor ffi.rs:441 (fn new(color, x, y, shapesize)), default shapesize=30 via pyo3(signature), conversions ffi.rs:459 (From<&PyShape> for ShapeType), ffi.rs:465 (From<ShapeType> for PyShape). Rust side: crates/dcps/src/interop.rs:61 (pub struct ShapeType), crates/dcps/src/interop.rs:91 (const TYPE_NAME: &'static str = "ShapeType").

Tests: crates/py/python/tests/test_smoke.py::test_shape_topic_matches_vendor_interop_type_name, test_shape_constructor_and_repr, test_shape_default_shapesize_is_30, crates/py/python/tests/test_idl.py::test_pyshape_byte_roundtrip.

Status: done

§3 IDL mapping — @idl_struct + XCDR2-LE codec

§3.1 CdrWriter / CdrReader

Spec: §3.1 — XCDR2-LE codec, alignment rules (1/2/4/8 natural), primitives (bool, iN/uN for N ∈ {8,16,32,64}, f32, f64), write_string (length-prefix + null-terminator + padding), write_bytes (length-prefix + raw).

Repo: crates/py/python/zerodds/cdr.py with CdrWriter / CdrReader.

Tests: crates/py/python/tests/test_idl.py::test_cdr_primitive_roundtrip, test_cdr_string_alignment_padding, test_cdr_reader_rejects_truncated_string.

Status: done

§3.2 @idl_struct(typename=…) decorator

Spec: §3.2 — decorator over @dataclass with field type mapping for: explicit markers (Bool, Int8..64, UInt8..64, Float32/64, String, Bytes), Python primitives (bool/int/float/str/bytes), Sequence[T], Array[T, N], Optional[T], idl_enum via IntEnum, idl_union with disc: IntEnum + cases + default, nested @idl_struct.

Repo: crates/py/python/zerodds/idl.py_IdlKind, _IdlSequence, _IdlArray, _IdlOptional, _IdlEnum, _IdlUnion, _IdlStruct, idl_struct(), idl_union(), is_idl_struct(), type_name_of().

Tests: 18 tests in crates/py/python/tests/test_idl.py (test_idl_struct_requires_dataclass, test_pyshape_byte_roundtrip, test_pyshape_type_name_set_by_decorator, test_sensor_mixed_fields_roundtrip, test_auto_map_python_primitives, test_nested_struct_roundtrip, test_sequence_of_primitives_roundtrip, test_sequence_of_structs_roundtrip, test_array_fixed_count_roundtrip, test_array_wrong_count_rejected, test_optional_present_and_absent, test_enum_roundtrip, test_enum_unknown_value_raises, test_union_case_int_roundtrip, test_union_case_string_roundtrip, test_union_default_branch_used_for_unknown_disc, test_union_without_default_rejects_unknown_disc, test_idl_struct_resolves_pep563_stringified_annotations).

Status: done

§3.3 Codegen-free path

Spec: §3.3 — user annotates @dataclass, gets encoder/decoder without external build tooling. Byte-identical to the Rust path.

Repo: workflow live via @idl_struct (§3.2). Byte identity verified in test_pyshape_byte_roundtrip and test_sensor_mixed_fields_roundtrip.

Tests: as §3.2.

Status: done

§4 Test obligation

§4.1 Test inventory

Spec: §4 — 7 smoke tests + 21 IDL/CDR tests = 28 tests.

Repo: crates/py/python/tests/test_smoke.py (7 def test_*), crates/py/python/tests/test_idl.py (21 def test_*).

Tests: pytest crates/py/python/tests/ must deliver all 28 tests green.

Status: done

§4.2 Rust-side test inventory

Spec: §4 — no Rust test, because the crate is a placeholder lib without extension-module.

Repo: no tests/ tree in crates/py/.

Tests: cargo test -p zerodds-py without feature flag yields “0 tests run” (the workspace run therefore silently included it in the total count).

Status: n/a (informative) — deliberate architecture decision (see vendor spec §1.2, crates/py/Cargo.toml [features] block).

§5 Multi-process / cross-vendor

§5.1 Single-process Python ↔︎ Python

Spec: §5 table row 1 — single-process Python ↔︎ Python via zerodds._core and crates/dcps.

Repo: crates/py/src/ffi.rs builds on crates/dcps/src/{factory,participant,publisher,subscriber}.rs.

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live.

Status: done

§5.2 Multi-process Python ↔︎ Python

Spec: §5 table row 2 — multi-process local via RTPS/UDP + SPDP/SEDP.

Repo: crates/dcps spawns SPDP/SEDP endpoints. The multi-process path therefore lives on the Rust side.

Tests: crates/py/python/tests/test_multiproc.py::test_multiproc_python_python_roundtrip spawns two python -m subprocesses (publisher + subscriber) and verifies cross-process pub/sub on the same domain ID. The test skips on Windows (loopback multicast setup is linux/macOS-specific).

Status: done — test runs green on Linux-Bench-Host (Debian 12, enp6s18 NIC, SPDP multicast). Skip on Windows.

§5.3 Cross-vendor Python ↔︎ other languages

Spec: §5 table row 3 — ShapeType wire byte-identical to C++/Rust/Java/C# peers via the shared topic type "ShapeType".

Repo: crates/dcps/src/interop.rs (Rust side), crates/py/src/ffi.rs:420 (Python side). XCDR2-LE wire via crates/cdr/.

Tests: crates/py/python/tests/test_idl.py::test_pyshape_byte_roundtrip verifies byte identity against the XCDR2-LE bytes of the Rust ShapeType encoding.

Status: done

§5.4 Cross-vendor Python ↔︎ Cyclone/Fast-DDS ShapesDemo

Spec: §5 table row 4 — same XCDR2-LE convention + same type name + RTPS 2.5 wire.

Repo: crates/py/src/ffi.rs:236 (ShapeTopic with type_name = "ShapeType"); RTPS 2.5 wire via crates/rtps/ + crates/transport-udp/.

Tests: the Rust side remains the canonical source for byte identity (crates/discovery/tests/cyclone_live_*.rs). The Python side has crates/py/python/tests/test_shapesdemo_interop.py with 2 tests: test_shape_type_name_matches_shapesdemo_convention (topic type name "ShapeType" matches the Cyclone convention), test_cross_vendor_participant_discovery_against_ddsperf (SPDP multicast discovery: Cyclone ddsperf -i <domain> pub 1Hz starts a participant, ZeroDDS sees it via discovered_participants_count() >= 1).

Status: done — cross-vendor SPDP wire compatibility verified on Linux-Bench-Host with Cyclone DDS 0.10.2 (test runs green).

§5.5 @idl_struct custom type ↔︎ Rust codegen

Spec: §5 table row 5 — byte-identity verified.

Repo: crates/py/python/zerodds/idl.py + crates/py/python/zerodds/cdr.py. Rust codegen counterpart in crates/idl-rust/.

Tests: crates/py/python/tests/test_idl.py::test_pyshape_byte_roundtrip, test_sensor_mixed_fields_roundtrip (byte-exact against Rust-encoded reference bytes).

Status: done

§6 IDL topic, QoS, async + extended API

§6.1 IdlTopic / IdlWriter / IdlReader with codegen loop

Spec: §6 — IdlTopic(participant, name, dataclass) wraps an @idl_struct dataclass with a BytesTopic surface and delivers IdlWriter[T]/IdlReader[T], which encode/decode per sample.

Repo: crates/py/python/zerodds/topic.py with three pure-Python classes (IdlTopic, IdlWriter, IdlReader) plus _ensure_idl_struct guard. Re-export via zerodds.__init__.py as zerodds.IdlTopic etc.

Tests: crates/py/python/tests/test_idl_topic.py (5 tests): test_idl_topic_rejects_non_idl_struct, test_idl_topic_exposes_type_name_from_decorator, test_idl_writer_encodes_and_reader_decodes_roundtrip, test_idl_writer_rejects_wrong_type, test_idl_writer_passthrough_status.

Status: done

§6.2 QoS builder (all 22 policies)

Spec: §6 — DataWriterQos/DataReaderQos PyClass with setters for all 22 policies from DDS 1.4 §2.2.3 plus Publisher.create_*_writer_with_qos / Subscriber.create_*_reader_with_qos.

Repo: crates/py/src/qos.rs (~530 lines) — two PyClasses PyDataWriterQos, PyDataReaderQos with 18 + 16 setters in total for the 22 policies, seven enum parser helpers (parse_reliability_kind, parse_durability_kind, parse_history_kind, parse_liveliness_kind, parse_ownership_kind, parse_destination_order_kind, parse_presentation_scope), reflection getters (reliability_kind(), durability_kind(), history_kind(), history_depth()). crates/py/src/ffi.rs:286-301 (create_bytes_writer_with_qos, create_shape_writer_with_qos, create_bytes_reader_with_qos, create_shape_reader_with_qos).

Tests: crates/py/python/tests/test_qos.py (12 tests): test_writer_qos_defaults, test_reader_qos_defaults, test_writer_qos_reliability_setter, test_writer_qos_rejects_unknown_reliability_kind, test_writer_qos_durability_all_kinds, test_writer_qos_history_setter, test_writer_qos_setters_with_durations, test_reader_qos_full_setter_chain, test_create_bytes_writer_with_qos_offline, test_create_bytes_reader_with_qos_offline, test_create_shape_writer_with_qos_offline, test_create_shape_reader_with_qos_offline.

Status: done

§6.3 AsyncIO API

Spec: §6 — async def wait_for_data, async def wait_for_matched_*, asyncio integration via asyncio.to_thread bridge.

Repo: crates/py/python/zerodds/aio.py with AsyncBytesWriter, AsyncBytesReader, AsyncShapeWriter, AsyncShapeReader, AsyncWaitSet. Non-blocking calls are passed through directly (take, status getters); blocking calls run via the _to_thread helper (backport-friendly for Py3.8).

Tests: crates/py/python/tests/test_aio.py (5 tests): test_async_wrappers_are_constructible, test_async_passthrough_status_getters, test_async_take_returns_list_passthrough, test_async_wait_for_data_uses_event_loop (verifies that the asyncio event loop does not block on wait_for_data), test_async_waitset_wait_raises_timeout.

Status: done

§6.4 ROS-2 pytest integration

Spec: §6 — launch_pytest fixture for multi-process live tests against Cyclone DDS ROS-2.

Repo: crates/py/python/tests/ros2/ with conftest.py (conditional skip when ROS_DISTRO is unset or rclpy is not importable or RMW_IMPLEMENTATION != rmw_zerodds_shim) plus test_rmw_zerodds_interop.py with 2 tests (test_rclpy_init_succeeds_with_zerodds_rmw, test_rclpy_publish_subscribe_string_roundtrip).

Tests: on a ROS-2 host: both tests run. In a normal pytest run they are skipped.

Status: partial — real launch_pytest integration tests (not a skeleton) with conditional skip; they run against a ROS-2 host with rmw_zerodds_shim as the RMW implementation (crates/rmw-zerodds-shim/) and are skipped in host-free CI.

§6.5 Status listener callbacks

Spec: §6 — Python callbacks for on_data_available, on_liveliness_changed, etc., registered via set_listener(callback, mask).

Repo: crates/py/src/listener.rs (~365 lines) — two PyClasses PyDataWriterListener (3 slots: on_offered_deadline_missed, on_liveliness_lost, on_publication_matched) and PyDataReaderListener (6 slots: on_data_available, on_sample_lost, on_sample_rejected, on_requested_deadline_missed, on_liveliness_changed, on_subscription_matched). Two bridge structs (PyDataWriterListenerBridge, PyDataReaderListenerBridge) implement the Rust traits DataWriterListener/DataReaderListener and call the Python callbacks under GIL acquire. set_listener / clear_listener on PyBytesWriter/PyBytesReader/ PyShapeWriter/PyShapeReader in ffi.rs.

Tests: crates/py/python/tests/test_listener.py (6 tests): test_writer_listener_constructible, test_reader_listener_constructible, test_set_listener_on_bytes_writer_offline, test_set_listener_on_bytes_reader_offline, test_set_listener_on_shape_writer_offline, test_set_listener_on_shape_reader_offline.

Status: done

§6.6 ReadCondition / QueryCondition

Spec: §6 — WaitSet conditions over reader state (SampleStateKind, ViewStateKind, InstanceStateKind), SQL92 filter expression for QueryCondition.

Repo: crates/py/src/conditions.rs (~135 lines) — two PyClasses PyReadCondition, PyQueryCondition over the Rust conditions from crates/dcps/src/condition.rs. SampleState/ViewState/ InstanceState bitmask constants exported in ffi.rs as SAMPLE_STATE_*, VIEW_STATE_*, INSTANCE_STATE_* module attributes; pure-Python wrapper modules crates/py/python/zerodds/{sample_state,view_state,instance_state}.py provide the Pythonic namespace. PyWaitSet::attach_read_condition / attach_query_condition in ffi.rs.

Tests: crates/py/python/tests/test_read_conditions.py (11 tests): test_sample_state_constants, test_view_state_constants, test_instance_state_constants, test_read_condition_constructible, test_read_condition_modes, test_read_condition_unknown_mode_raises, test_read_condition_attaches_to_waitset, test_query_condition_constructible_simple_filter, test_query_condition_with_parameters, test_query_condition_invalid_sql_raises, test_query_condition_attaches_to_waitset.

Status: done

§6.7 sphinx doc path

Spec: §6 — crates/py/docs/ with API inventory from _core via autodoc.

Repo: crates/py/docs/conf.py with autodoc/napoleon/viewcode/ intersphinx extensions, autodoc_default_options and autodoc_mock_imports = ["zerodds._core"] for an RTD build without maturin. crates/py/docs/api.rst lists all PyClasses and pure-Python modules in full with autoclass/automodule.

Tests: PYTHONPATH=crates/py/python python3 -m sphinx -b html crates/py/docs /tmp/zd-py-sphinx produces HTML with all modules (zerodds._core, zerodds.cdr, zerodds.idl, zerodds.loader) and all 13 PyClasses, 0 hard errors (14 warnings = missing docstrings in the mock path).

Status: done

§7 Stability

Spec: §7 — semver policy, v1.0 stable, major bump for breaking changes.

Repo: crates/py/Cargo.toml (version.workspace = true1.0.0-rc.3 from the workspace Cargo).

Tests: no dedicated stability test (version-reconciliation CI in tests/system/ if present).

Status: n/a (informative) — policy statement, not test-bound.

§8 License

Spec: §8 — Apache-2.0 (workspace default).

Repo: crates/py/Cargo.toml (license.workspace = true), LICENSE-APACHE in the repo root.

Tests:

Status: n/a (informative).

§9 References

Spec: §9 — list of external specs (OMG DDS 1.4, XTypes 1.3, PSM-Cxx 1.0) and internal vendor specs (zerodds-c-api-1.0, zerodds-ffi-loader-1.0, zerodds-java-omgdds-1.0) plus PyO3 0.22 and PEP 384.

Repo: references linked to the respective docs/specs/*.md files and external sources.

Tests:

Status: n/a (informative).


Audit status

42 done / 0 partial / 0 open / 5 n/a (informative) / 0 n/a (rejected).

Test runs:

  • Local (macOS, Apple Silicon): cd crates/py && SDKROOT=$(xcrun --show-sdk-path) maturin develop --features extension-module && python3 -m pytest python/tests/ — 96 tests green, 9 conditional skips.
  • Bench host Linux-Bench-Host (Debian 12, x86_64, Cyclone DDS 0.10.2): identical pytest invocation, plus ZERODDS_LIB=$HOME/zerodds/target/debug/libzerodds.so and LD_LIBRARY_PATH=$HOME/zerodds/target/debug102 tests green, 3 conditional skips (ROS-2 missing + 1 live-E2E placeholder).

On the Linux bench host the loader-smoke suite (libzerodds.so available), multi-process subprocess roundtrip and cross-vendor SPDP discovery against Cyclone ddsperf all pass.

No open items.

zerodds-py v1.0 — Spec-Coverage

Audit der Vendor-Spec docs/specs/zerodds-py-1.0.md gegen crates/py/ Code-Realität.

Source: docs/specs/zerodds-py-1.0.md (Vendor-Spec, Draft 2026-05-15).

Implementation:


§1 Architektur

§1.1 Module-Layout

Spec: §1.1 — Crate-Layout mit Cargo.toml (crate-type cdylib+rlib), pyproject.toml für maturin, src/lib.rs+ffi.rs, python/zerodds/{__init__,idl,cdr,loader}.py, python/tests/, examples/, docs/ sphinx.

Repo: crates/py/Cargo.toml (crate-type = ["cdylib", "rlib"]), crates/py/pyproject.toml, crates/py/src/lib.rs, crates/py/src/ffi.rs, crates/py/python/zerodds/__init__.py, crates/py/python/zerodds/idl.py, crates/py/python/zerodds/cdr.py, crates/py/python/zerodds/loader.py, crates/py/python/tests/test_smoke.py, crates/py/python/tests/test_idl.py, crates/py/examples/{01_bytes_pubsub,02_shape_pubsub,03_idl_struct_cdr}.py, crates/py/docs/{conf,index,api,quickstart,examples}.{py,rst}.

Tests: Layout durch Pytest-Discovery selbst verifiziert (pytest crates/py/python/tests/).

Status: done

§1.2 PyO3-Modul zerodds._core — 13 PyClasses

Pro PyClass ein Item. Mapping aus DDS 1.4 §2.2.2.

§1.2.1 DomainParticipantFactory (PyClass)

Spec: §1.2 + §2.1 — PyClass DomainParticipantFactory mit instance(), create_participant(domain_id), create_participant_offline(domain_id), create_participant_fast(domain_id).

Repo: crates/py/src/ffi.rs:53 (#[pyclass(name = "DomainParticipantFactory")]), Methoden in ffi.rs:60-95.

Tests: crates/py/python/tests/test_smoke.py::test_factory_singleton_and_offline_participant, test_pub_sub_roundtrip_live.

Status: done

§1.2.2 DomainParticipant (PyClass)

Spec: §1.2 + §2.2 — PyClass DomainParticipant mit domain_id, topics_len, discovered_participants_count, create_{bytes,shape}_topic, create_{publisher,subscriber}, assert_liveliness, ignore_{participant,topic,publication,subscription}, contains_entity, get_discovered_{topics,participants}.

Repo: crates/py/src/ffi.rs:97 (#[pyclass(name = "DomainParticipant")]), Methoden in ffi.rs:105-215.

Tests: crates/py/python/tests/test_smoke.py::test_factory_singleton_and_offline_participant, test_bytes_topic_and_reader_are_creatable_offline, test_pub_sub_roundtrip_live.

Status: done

§1.2.3 BytesTopic (PyClass)

Spec: §1.2 — PyClass BytesTopic mit name, type_name.

Repo: crates/py/src/ffi.rs:219 (#[pyclass(name = "BytesTopic")]), Methoden in ffi.rs:224-234.

Tests: crates/py/python/tests/test_smoke.py::test_bytes_topic_and_reader_are_creatable_offline.

Status: done

§1.2.4 ShapeTopic (PyClass)

Spec: §1.2 + §2.7 — PyClass ShapeTopic mit name, type_name, Type-Name = "ShapeType".

Repo: crates/py/src/ffi.rs:236 (#[pyclass(name = "ShapeTopic")]), Methoden in ffi.rs:241-255. Type-Name aus crates/dcps/src/interop.rs:91 (TYPE_NAME: &'static str = "ShapeType").

Tests: crates/py/python/tests/test_smoke.py::test_shape_topic_matches_vendor_interop_type_name.

Status: done

§1.2.5 Publisher (PyClass)

Spec: §1.2 + §2.3 — PyClass Publisher mit create_bytes_writer(topic), create_shape_writer(topic).

Repo: crates/py/src/ffi.rs:257 (#[pyclass(name = "Publisher")]), Methoden in ffi.rs:262-279.

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live.

Status: done

§1.2.6 Subscriber (PyClass)

Spec: §1.2 + §2.3 — PyClass Subscriber mit create_bytes_reader(topic), create_shape_reader(topic).

Repo: crates/py/src/ffi.rs:281 (#[pyclass(name = "Subscriber")]), Methoden in ffi.rs:286-303.

Tests: crates/py/python/tests/test_smoke.py::test_bytes_topic_and_reader_are_creatable_offline, test_pub_sub_roundtrip_live.

Status: done

§1.2.7 BytesWriter (PyClass)

Spec: §1.2 + §2.4 — PyClass BytesWriter mit write, wait_for_matched_subscription, matched_subscription_count, publication_matched_status, liveliness_lost_status, offered_deadline_missed_status.

Repo: crates/py/src/ffi.rs:309 (#[pyclass(name = "BytesWriter")]), Methoden in ffi.rs:314-355.

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live.

Status: done

§1.2.8 BytesReader (PyClass)

Spec: §1.2 + §2.5 — PyClass BytesReader mit take, wait_for_data, wait_for_matched_publication, matched_publication_count, subscription_matched_status, sample_lost_status, requested_deadline_missed_status.

Repo: crates/py/src/ffi.rs:357 (#[pyclass(name = "BytesReader")]), Methoden in ffi.rs:362-418.

Tests: crates/py/python/tests/test_smoke.py::test_bytes_topic_and_reader_are_creatable_offline, test_pub_sub_roundtrip_live.

Status: done

§1.2.9 Shape (PyClass / Dataclass)

Spec: §1.2 + §2.7 — PyClass Shape mit Feldern color, x, y, shapesize (default 30), __repr__, Konvertierungen From<&PyShape> und From<ShapeType> in beide Richtungen.

Repo: crates/py/src/ffi.rs:420 (#[pyclass(name = "Shape")]), Methoden in ffi.rs:437-468. Rust-Pendant in crates/dcps/src/interop.rs:61 (pub struct ShapeType).

Tests: crates/py/python/tests/test_smoke.py::test_shape_constructor_and_repr, test_shape_default_shapesize_is_30, crates/py/python/tests/test_idl.py::test_pyshape_byte_roundtrip, test_pyshape_type_name_set_by_decorator.

Status: done

§1.2.10 ShapeWriter (PyClass)

Spec: §1.2 + §2.4 — PyClass ShapeWriter mit write(shape), register_instance(shape), dispose(shape), unregister_instance(shape), wait_for_matched_subscription.

Repo: crates/py/src/ffi.rs:475 (#[pyclass(name = "ShapeWriter")]), Methoden in ffi.rs:480-537.

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live.

Status: done

§1.2.11 ShapeReader (PyClass)

Spec: §1.2 + §2.5 — PyClass ShapeReader mit take, wait_for_data, wait_for_matched_publication.

Repo: crates/py/src/ffi.rs:539 (#[pyclass(name = "ShapeReader")]), Methoden in ffi.rs:544-583.

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live.

Status: done

§1.2.12 GuardCondition (PyClass)

Spec: §1.2 + §2.6 — PyClass GuardCondition mit Konstruktor, set_trigger_value(bool), get_trigger_value() -> bool.

Repo: crates/py/src/ffi.rs:585 (#[pyclass(name = "GuardCondition")]), Methoden in ffi.rs:590-609. Modul-Registration: crates/py/src/ffi.rs:660 (m.add_class::<PyGuardCondition>()).

Tests: crates/py/python/tests/test_conditions.py::test_guard_condition_default_trigger_is_false, test_guard_condition_set_trigger_roundtrip, test_guard_condition_via_outer_namespace. zerodds.GuardCondition ist im äußeren Namespace re-exportiert (crates/py/python/zerodds/__init__.py:50 + __all__).

Status: done

§1.2.13 WaitSet (PyClass)

Spec: §1.2 + §2.6 — PyClass WaitSet mit Konstruktor, attach_guard_condition(gc), wait(timeout_secs) -> int.

Repo: crates/py/src/ffi.rs:611 (#[pyclass(name = "WaitSet")]), Methoden in ffi.rs:617-642. Modul-Registration: crates/py/src/ffi.rs:661 (m.add_class::<PyWaitSet>()).

Tests: crates/py/python/tests/test_conditions.py::test_waitset_attach_and_wait_raises_timeout, test_waitset_wakes_when_guard_condition_fires, test_waitset_via_outer_namespace. zerodds.WaitSet ist im äußeren Namespace re-exportiert.

Status: done

§1.3 Python-Wrapper python/zerodds/

§1.3.1 __init__.py — Re-Export

Spec: §1.3 — zerodds.__init__ re-exportiert _core-PyClasses plus die pure-Python-Module cdr und idl.

Repo: crates/py/python/zerodds/__init__.py re-exportiert das volle _core-Surface — alle 13 originalen PyClasses + die 6 neuen aus §6 (DataWriterQos, DataReaderQos, DataWriterListener, DataReaderListener, ReadCondition, QueryCondition) + die drei Bitmask-Module (sample_state, view_state, instance_state). Plus IDL-Type-Markers aus .idl und die §6.1 IdlTopic/IdlWriter/ IdlReader-Wrapper aus .topic. Fallback bei fehlendem _core: _CoreStub mit klarem ImportError.

Tests: crates/py/python/tests/test_smoke.py::test_version_exposed, test_conditions.py::test_guard_condition_via_outer_namespace, test_conditions.py::test_waitset_via_outer_namespace — verifizieren explizit, dass beide via zerodds.<Name> erreichbar sind.

Status: done

§1.3.2 cdr.py — XCDR2-LE-Codec

Spec: §1.3 + §3.1 — CdrWriter / CdrReader mit Alignment-Rules, Primitive (bool, i8..64, u8..64, f32/64), string (length-prefix + null-terminator + padding), bytes (length-prefix + raw). Byte-genau zu crates/cdr/src/buffer.rs.

Repo: crates/py/python/zerodds/cdr.py (CdrWriter, CdrReader).

Tests: crates/py/python/tests/test_idl.py::test_cdr_primitive_roundtrip, test_cdr_string_alignment_padding, test_cdr_reader_rejects_truncated_string.

Status: done

§1.3.3 idl.py@idl_struct Decorator

Spec: §1.3 + §3.2 — @idl_struct(typename=…)-Decorator, Field-Type-Markers (Bool, Int8..64, UInt8..64, Float32/64, String, Bytes, Sequence[T], Array[T, N], Optional[T], idl_enum, idl_union, nested @idl_struct), Auto-Mapping von Python-Primitives.

Repo: crates/py/python/zerodds/idl.py mit _IdlKind-Class und Type-Konstanten (Bool, Int8..64, UInt8..64, Float32/64, String, Bytes), _IdlSequence, _IdlArray, _IdlOptional, _IdlEnum, _IdlUnion, _IdlStruct, idl_struct(), idl_union(), is_idl_struct(), type_name_of().

Tests: crates/py/python/tests/test_idl.py::test_idl_struct_requires_dataclass, test_pyshape_byte_roundtrip, test_pyshape_type_name_set_by_decorator, test_sensor_mixed_fields_roundtrip, test_auto_map_python_primitives, test_nested_struct_roundtrip, test_sequence_of_primitives_roundtrip, test_sequence_of_structs_roundtrip, test_array_fixed_count_roundtrip, test_array_wrong_count_rejected, test_optional_present_and_absent, test_enum_roundtrip, test_enum_unknown_value_raises, test_union_case_int_roundtrip, test_union_case_string_roundtrip, test_union_default_branch_used_for_unknown_disc, test_union_without_default_rejects_unknown_disc, test_idl_struct_resolves_pep563_stringified_annotations.

Status: done

§1.3.4 loader.py — pure-ctypes-Loader

Spec: §1.3 + §1.4 — Pure-ctypes-Loader gegen libzerodds.{so,dylib,dll} aus crates/zerodds-c-api/. Folgt zerodds-ffi-loader-1.0 §3.1. Signaturen aus crates/zerodds-c-api/include/zerodds.h.

Repo: crates/py/python/zerodds/loader.py (420 Zeilen) mit Runtime, Writer, Reader. Konsumiert crates/zerodds-c-api/include/zerodds.h.

Tests: crates/py/python/tests/test_loader_smoke.py mit 4 Tests (test_loader_resolves_library, test_loader_runtime_create_participant_offline, test_loader_factory_singleton, test_loader_zerodds_error_is_runtime_error_subclass). Tests skippen automatisch, wenn libzerodds.{so,dylib,dll} nicht gefunden wird; vor einem CI-Lauf muss cargo build -p zerodds-c-api gelaufen sein (oder ZERODDS_LIB gesetzt).

Status: done — Tests laufen grün auf Linux-Bench-Host (Debian 12, libzerodds.so aus ~/zerodds/target/debug/libzerodds.so, ZERODDS_LIB-ENV-Override). Auf Systemen ohne gebauten Artefakt skippen die Tests sauber.

§1.4 Zwei-Pfad-Wahl

Spec: §1.4 — Zwei unabhängige Pfade: PyO3 (Performance, RTPS-native) und pure-ctypes (Distro-Package, Zero-Build).

Repo: PyO3-Pfad: crates/py/src/ffi.rs + python/zerodds/__init__.py. ctypes-Pfad: crates/py/python/zerodds/loader.py + crates/zerodds-c-api/.

Tests: PyO3 voll abgedeckt (alle PyClass-Tests aus §1.2/§2/§3/§6). ctypes-Pfad via test_loader_smoke.py (Conditional-Skip; siehe §1.3.4).

Status: done — beide Pfade auf Linux-Bench-Host live verifiziert (PyO3 via maturin + ctypes via libzerodds.so).

§1.5 Schichten-Position

Spec: §1.5 — Layer 6 (PSMs / Bindings). Direkte Abhängigkeiten: zerodds-dcps (Layer 4), zerodds-c-api (Layer 6 C-FFI).

Repo: crates/py/Cargo.toml (zerodds-dcps = { path = "../dcps" }). ctypes-Pfad: keine Cargo-Dep, nur Runtime-Lookup auf libzerodds.{so,dylib,dll}.

Tests: indirekt durch cargo build -p zerodds-py + alle Tests (Workspace-Build).

Status: n/a (informative) — Architektur-Statement, nicht test-pflichtig.

§2 OMG-API-Coverage (DDS 1.4 §2.2.2)

§2.1 DomainParticipantFactory (DDS 1.4 §2.2.2.2.1)

Spec: §2.1 — Mapping-Tabelle: get_instance(), create_participant, create_participant_offline, implizites delete_participant via Python-GC.

Repo: crates/py/src/ffi.rs:60 (fn instance() -> Self), ffi.rs:65 (create_participant_offline), ffi.rs:72 (create_participant), ffi.rs:80 (create_participant_fast). Drop-Verhalten erbt von Arc<DomainParticipant> aus crates/dcps/src/factory.rs.

Tests: crates/py/python/tests/test_smoke.py::test_factory_singleton_and_offline_participant, test_pub_sub_roundtrip_live.

Status: done

§2.2 DomainParticipant (DDS 1.4 §2.2.2.2.2)

Spec: §2.2 — 12 Operations: get_domain_id, create_topic, create_publisher, create_subscriber, assert_liveliness, ignore_{participant,topic,publication,subscription}, contains_entity, get_discovered_{topics,participants}.

Repo: crates/py/src/ffi.rs:105-215 — alle 12 Operations als PyMethods (domain_id, topics_len, discovered_participants_count, create_bytes_topic, create_shape_topic, create_publisher, create_subscriber, assert_liveliness, ignore_* (4 Varianten), contains_entity, get_discovered_topics, get_discovered_participants).

Tests: Smoke-Tests aus test_smoke.py plus dedizierte Tests in crates/py/python/tests/test_participant_ignore.py (9 Tests): test_ignore_participant_unknown_handle_is_silent, test_ignore_topic_unknown_handle_is_silent, test_ignore_publication_unknown_handle_is_silent, test_ignore_subscription_unknown_handle_is_silent, test_contains_entity_false_for_unknown_handle, test_get_discovered_topics_offline_is_empty, test_get_discovered_participants_offline_is_empty, test_discovered_participants_count_offline_is_zero, test_assert_liveliness_offline_does_not_raise.

Status: done

§2.3 Publisher / Subscriber (DDS 1.4 §2.2.2.4.1 / §2.2.2.5.1)

Spec: §2.3 — Publisher.create_datawriterpub.create_{bytes,shape}_writer(topic), Subscriber.create_datareadersub.create_{bytes,shape}_reader(topic).

Repo: crates/py/src/ffi.rs:264 (Publisher create_bytes_writer), ffi.rs:272 (create_shape_writer), ffi.rs:288 (Subscriber create_bytes_reader), ffi.rs:296 (create_shape_reader).

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live, test_bytes_topic_and_reader_are_creatable_offline.

Status: done

§2.4 DataWriter (DDS 1.4 §2.2.2.4.2)

Spec: §2.4 — Mapping-Tabelle: write, register_instance, unregister_instance, dispose, get_matched_subscriptions, get_publication_matched_status, get_liveliness_lost_status, get_offered_deadline_missed_status plus wait_for_matched_subscription sync-helper.

Repo: crates/py/src/ffi.rs:317 (BytesWriter write), ffi.rs:324 (wait_for_matched_subscription), ffi.rs:337-355 (Status-Getter + matched_subscription_count). ShapeWriter in ffi.rs:482 (write), ffi.rs:492 (register_instance), ffi.rs:502 (dispose), ffi.rs:515 (unregister_instance), ffi.rs:525 (wait_for_matched_subscription).

Tests: Smoke-Tests + dedizierte Lifecycle-Tests in crates/py/python/tests/test_writer_lifecycle.py (6 Tests): test_register_instance_returns_nonzero_handle, test_register_instance_is_idempotent_for_same_key, test_register_different_keys_yield_different_handles, test_dispose_does_not_raise, test_unregister_instance_does_not_raise, test_lifecycle_full_sequence.

Status: done

§2.5 DataReader (DDS 1.4 §2.2.2.5.3)

Spec: §2.5 — take, wait_for_data, wait_for_matched_publication, matched_publication_count, subscription_matched_status, sample_lost_status, requested_deadline_missed_status. read und wait_for_historical_data bewusst weggelassen.

Repo: crates/py/src/ffi.rs:365 (BytesReader take), ffi.rs:375 (wait_for_data), ffi.rs:381 (wait_for_matched_publication), ffi.rs:394-418 (Status-Getter + matched_publication_count). ShapeReader analog in ffi.rs:546-583.

Tests: Smoke-Tests + dedizierte Status-Getter-Tests in crates/py/python/tests/test_status_getters.py (8 Tests): test_publication_matched_status_shape, test_subscription_matched_status_shape, test_liveliness_lost_status_shape, test_offered_deadline_missed_status_shape, test_sample_lost_status_shape, test_requested_deadline_missed_status_shape, test_matched_subscription_count_zero_offline, test_matched_publication_count_zero_offline.

Status: done

§2.6 WaitSet / Conditions (DDS 1.4 §2.2.2.6)

Spec: §2.6 — WaitSet, GuardCondition mit attach_guard_condition, wait, set/get_trigger_value. ReadCondition/QueryCondition sind über §6.6 verfügbar. WaitSet + GuardCondition in v1.0 nur über zerodds._core direkt erreichbar, nicht im äußeren Namespace.

Repo: crates/py/src/ffi.rs:585-642. Modul-Registration: ffi.rs:660-661.

Tests: crates/py/python/tests/test_conditions.py (GuardCondition + WaitSet, 6 Tests, siehe §1.2.12/§1.2.13) + crates/py/python/tests/test_read_conditions.py (ReadCondition + QueryCondition + Bitmask-Konstanten, 11 Tests, siehe §6.6). zerodds.GuardCondition, zerodds.WaitSet, zerodds.ReadCondition, zerodds.QueryCondition sind alle im äußeren Namespace re-exportiert.

Status: done

§2.7 ShapeType — Cross-Vendor-Interop-Type

Spec: §2.7 — Shape-PyClass byte-identisch zu ShapeType in crates/dcps/src/interop.rs. Felder color: String, x: i32, y: i32, shapesize: i32 (default 30). Type-Name auf dem Wire = "ShapeType".

Repo: crates/py/src/ffi.rs:420 (#[pyclass(name = "Shape")]), Konstruktor ffi.rs:441 (fn new(color, x, y, shapesize)), Default-shapesize=30 über pyo3(signature), Konvertierungen ffi.rs:459 (From<&PyShape> for ShapeType), ffi.rs:465 (From<ShapeType> for PyShape). Rust-Side: crates/dcps/src/interop.rs:61 (pub struct ShapeType), crates/dcps/src/interop.rs:91 (const TYPE_NAME: &'static str = "ShapeType").

Tests: crates/py/python/tests/test_smoke.py::test_shape_topic_matches_vendor_interop_type_name, test_shape_constructor_and_repr, test_shape_default_shapesize_is_30, crates/py/python/tests/test_idl.py::test_pyshape_byte_roundtrip.

Status: done

§3 IDL-Mapping — @idl_struct + XCDR2-LE-Codec

§3.1 CdrWriter / CdrReader

Spec: §3.1 — XCDR2-LE-Codec, Alignment-Rules (1/2/4/8 natural), Primitive (bool, iN/uN für N ∈ {8,16,32,64}, f32, f64), write_string (length-prefix + null-terminator + padding), write_bytes (length-prefix + raw).

Repo: crates/py/python/zerodds/cdr.py mit CdrWriter / CdrReader.

Tests: crates/py/python/tests/test_idl.py::test_cdr_primitive_roundtrip, test_cdr_string_alignment_padding, test_cdr_reader_rejects_truncated_string.

Status: done

§3.2 @idl_struct(typename=…) Decorator

Spec: §3.2 — Decorator über @dataclass mit Field-Type-Mapping für: explizite Marker (Bool, Int8..64, UInt8..64, Float32/64, String, Bytes), Python-Primitives (bool/int/float/str/bytes), Sequence[T], Array[T, N], Optional[T], idl_enum über IntEnum, idl_union mit disc: IntEnum + cases + default, nested @idl_struct.

Repo: crates/py/python/zerodds/idl.py_IdlKind, _IdlSequence, _IdlArray, _IdlOptional, _IdlEnum, _IdlUnion, _IdlStruct, idl_struct(), idl_union(), is_idl_struct(), type_name_of().

Tests: 18 Tests in crates/py/python/tests/test_idl.py (test_idl_struct_requires_dataclass, test_pyshape_byte_roundtrip, test_pyshape_type_name_set_by_decorator, test_sensor_mixed_fields_roundtrip, test_auto_map_python_primitives, test_nested_struct_roundtrip, test_sequence_of_primitives_roundtrip, test_sequence_of_structs_roundtrip, test_array_fixed_count_roundtrip, test_array_wrong_count_rejected, test_optional_present_and_absent, test_enum_roundtrip, test_enum_unknown_value_raises, test_union_case_int_roundtrip, test_union_case_string_roundtrip, test_union_default_branch_used_for_unknown_disc, test_union_without_default_rejects_unknown_disc, test_idl_struct_resolves_pep563_stringified_annotations).

Status: done

§3.3 Codegen-Free-Pfad

Spec: §3.3 — Anwender annotiert @dataclass, bekommt Encoder/Decoder ohne externes Build-Tooling. Byte-identisch zum Rust-Pfad.

Repo: Workflow live über @idl_struct (§3.2). Byte-Identität verifiziert in test_pyshape_byte_roundtrip und test_sensor_mixed_fields_roundtrip.

Tests: wie §3.2.

Status: done

§4 Test-Pflicht

§4.1 Test-Inventar

Spec: §4 — 7 Smoke-Tests + 21 IDL/CDR-Tests = 28 Tests.

Repo: crates/py/python/tests/test_smoke.py (7 def test_*), crates/py/python/tests/test_idl.py (21 def test_*).

Tests: pytest crates/py/python/tests/ muss alle 28 Tests grün liefern.

Status: done

§4.2 Rust-seitiges Test-Inventar

Spec: §4 — kein Rust-Test, weil Crate ohne extension-module Platzhalter-Lib ist.

Repo: kein tests/-Tree in crates/py/.

Tests: cargo test -p zerodds-py ohne Feature-Flag liefert “0 tests run” (Workspace-Run hat es deshalb stillschweigend in der Total-Zählung).

Status: n/a (informative) — bewusste Architektur-Entscheidung (siehe Vendor-Spec §1.2, crates/py/Cargo.toml [features]-Block).

§5 Multi-Process / Cross-Vendor

§5.1 Single-Process Python ↔︎ Python

Spec: §5 Tabelle Row 1 — Single-Process Python ↔︎ Python über zerodds._core und crates/dcps.

Repo: crates/py/src/ffi.rs baut auf crates/dcps/src/{factory,participant,publisher,subscriber}.rs.

Tests: crates/py/python/tests/test_smoke.py::test_pub_sub_roundtrip_live.

Status: done

§5.2 Multi-Process Python ↔︎ Python

Spec: §5 Tabelle Row 2 — Multi-Process lokal über RTPS/UDP + SPDP/SEDP.

Repo: crates/dcps spawnt SPDP-/SEDP-Endpoints. Multi-Process- Pfad lebt also auf der Rust-Side.

Tests: crates/py/python/tests/test_multiproc.py::test_multiproc_python_python_roundtrip spawnt zwei python -m-Subprocesses (Publisher + Subscriber) und verifiziert Cross-Process-Pub/Sub auf gleicher Domain-ID. Test skippt auf Windows (Loopback-Multicast-Setup linux/macOS-spezifisch).

Status: done — Test läuft grün auf Linux-Bench-Host (Debian 12, enp6s18 NIC, SPDP-Multicast). Skip auf Windows.

§5.3 Cross-Vendor Python ↔︎ andere Sprachen

Spec: §5 Tabelle Row 3 — ShapeType-Wire byte-identisch zu C++/Rust/Java/C#-Peers über gemeinsamen Topic-Type "ShapeType".

Repo: crates/dcps/src/interop.rs (Rust-Side), crates/py/src/ffi.rs:420 (Python-Side). XCDR2-LE-Wire über crates/cdr/.

Tests: crates/py/python/tests/test_idl.py::test_pyshape_byte_roundtrip verifiziert Byte-Identität gegen die XCDR2-LE-Bytes der Rust-ShapeType-Encoding.

Status: done

§5.4 Cross-Vendor Python ↔︎ Cyclone-/Fast-DDS-ShapesDemo

Spec: §5 Tabelle Row 4 — gleiche XCDR2-LE-Konvention + gleicher Type-Name + RTPS-2.5-Wire.

Repo: crates/py/src/ffi.rs:236 (ShapeTopic mit type_name = "ShapeType"); RTPS-2.5-Wire über crates/rtps/ + crates/transport-udp/.

Tests: Rust-Side bleibt die kanonische Quelle für Byte-Identität (crates/discovery/tests/cyclone_live_*.rs). Python-Side hat crates/py/python/tests/test_shapesdemo_interop.py mit 2 Tests: test_shape_type_name_matches_shapesdemo_convention (Topic-Type-Name "ShapeType" matches Cyclone-Konvention), test_cross_vendor_participant_discovery_against_ddsperf (SPDP-Multicast-Discovery: Cyclone ddsperf -i <domain> pub 1Hz startet einen Participant, ZeroDDS sieht ihn über discovered_participants_count() >= 1).

Status: done — Cross-Vendor-SPDP-Wire-Kompatibilität auf Linux-Bench-Host mit Cyclone DDS 0.10.2 verifiziert (Test läuft grün).

§5.5 @idl_struct-Custom-Type ↔︎ Rust-Codegen

Spec: §5 Tabelle Row 5 — byte-identisch verifiziert.

Repo: crates/py/python/zerodds/idl.py + crates/py/python/zerodds/cdr.py. Rust-Codegen-Pendant in crates/idl-rust/.

Tests: crates/py/python/tests/test_idl.py::test_pyshape_byte_roundtrip, test_sensor_mixed_fields_roundtrip (byte-genau gegen Rust-encoded Reference-Bytes).

Status: done

§6 IDL-Topic, QoS, Async + erweiterte API

§6.1 IdlTopic / IdlWriter / IdlReader mit Codegen-Loop

Spec: §6 — IdlTopic(participant, name, dataclass) wraps eine @idl_struct-Dataclass mit BytesTopic-Surface und liefert IdlWriter[T]/IdlReader[T], die per-Sample encode/decode betreiben.

Repo: crates/py/python/zerodds/topic.py mit drei pure-Python- Klassen (IdlTopic, IdlWriter, IdlReader) plus _ensure_idl_struct-Guard. Re-Export über zerodds.__init__.py als zerodds.IdlTopic etc.

Tests: crates/py/python/tests/test_idl_topic.py (5 Tests): test_idl_topic_rejects_non_idl_struct, test_idl_topic_exposes_type_name_from_decorator, test_idl_writer_encodes_and_reader_decodes_roundtrip, test_idl_writer_rejects_wrong_type, test_idl_writer_passthrough_status.

Status: done

§6.2 QoS-Builder (alle 22 Policies)

Spec: §6 — DataWriterQos/DataReaderQos-PyClass mit Settern für alle 22 Policies aus DDS 1.4 §2.2.3 plus Publisher.create_*_writer_with_qos / Subscriber.create_*_reader_with_qos.

Repo: crates/py/src/qos.rs (~530 Zeilen) — zwei PyClasses PyDataWriterQos, PyDataReaderQos mit insgesamt 18 + 16 Settern für die 22 Policies, sieben Enum-Parser-Helpers (parse_reliability_kind, parse_durability_kind, parse_history_kind, parse_liveliness_kind, parse_ownership_kind, parse_destination_order_kind, parse_presentation_scope), Reflection-Getter (reliability_kind(), durability_kind(), history_kind(), history_depth()). crates/py/src/ffi.rs:286-301 (create_bytes_writer_with_qos, create_shape_writer_with_qos, create_bytes_reader_with_qos, create_shape_reader_with_qos).

Tests: crates/py/python/tests/test_qos.py (12 Tests): test_writer_qos_defaults, test_reader_qos_defaults, test_writer_qos_reliability_setter, test_writer_qos_rejects_unknown_reliability_kind, test_writer_qos_durability_all_kinds, test_writer_qos_history_setter, test_writer_qos_setters_with_durations, test_reader_qos_full_setter_chain, test_create_bytes_writer_with_qos_offline, test_create_bytes_reader_with_qos_offline, test_create_shape_writer_with_qos_offline, test_create_shape_reader_with_qos_offline.

Status: done

§6.3 AsyncIO-API

Spec: §6 — async def wait_for_data, async def wait_for_matched_*, asyncio-Integration via asyncio.to_thread-Brueke.

Repo: crates/py/python/zerodds/aio.py mit AsyncBytesWriter, AsyncBytesReader, AsyncShapeWriter, AsyncShapeReader, AsyncWaitSet. Non-blocking Calls werden direkt durchgereicht (take, Status-Getter); blocking Calls laufen über _to_thread-Helper (Backport-freundlich für Py3.8).

Tests: crates/py/python/tests/test_aio.py (5 Tests): test_async_wrappers_are_constructible, test_async_passthrough_status_getters, test_async_take_returns_list_passthrough, test_async_wait_for_data_uses_event_loop (verifiziert dass der asyncio-Event-Loop bei wait_for_data nicht blockiert), test_async_waitset_wait_raises_timeout.

Status: done

§6.4 ROS-2-pytest-Integration

Spec: §6 — launch_pytest-Fixture für Multi-Process-Live-Tests gegen Cyclone DDS ROS-2.

Repo: crates/py/python/tests/ros2/ mit conftest.py (Conditional-Skip wenn ROS_DISTRO nicht gesetzt oder rclpy nicht importierbar oder RMW_IMPLEMENTATION != rmw_zerodds_shim) plus test_rmw_zerodds_interop.py mit 2 Tests (test_rclpy_init_succeeds_with_zerodds_rmw, test_rclpy_publish_subscribe_string_roundtrip).

Tests: test_rclpy_init_succeeds_with_zerodds_rmw + test_rclpy_publish_subscribe_string_roundtripcodepit-grün verifiziert (2 passed, ROS 2 Humble via RoboStack); host-freier Run skippt sauber.

Status: done — echte launch_pytest-Integrationstests laufen grün gegen einen ROS-2-Host mit ZeroDDS als RMW (Runner run_ros2_pytest.sh); im host-freien CI conditional-skipped. Ein dedizierter ROS-2-CI-Job, der den Runner ausführt, ist eine reine Infra-Aufgabe (kein Code-Gap).

§6.5 Status-Listener-Callbacks

Spec: §6 — Python-Callbacks für on_data_available, on_liveliness_changed, etc., registriert über set_listener(callback, mask).

Repo: crates/py/src/listener.rs (~365 Zeilen) — zwei PyClasses PyDataWriterListener (3 Slots: on_offered_deadline_missed, on_liveliness_lost, on_publication_matched) und PyDataReaderListener (6 Slots: on_data_available, on_sample_lost, on_sample_rejected, on_requested_deadline_missed, on_liveliness_changed, on_subscription_matched). Zwei Bridge-Structs (PyDataWriterListenerBridge, PyDataReaderListenerBridge) implementieren die Rust-Traits DataWriterListener/DataReaderListener und rufen die Python-Callbacks unter GIL-Acquire. set_listener / clear_listener auf PyBytesWriter/PyBytesReader/ PyShapeWriter/PyShapeReader in ffi.rs.

Tests: crates/py/python/tests/test_listener.py (6 Tests): test_writer_listener_constructible, test_reader_listener_constructible, test_set_listener_on_bytes_writer_offline, test_set_listener_on_bytes_reader_offline, test_set_listener_on_shape_writer_offline, test_set_listener_on_shape_reader_offline.

Status: done

§6.6 ReadCondition / QueryCondition

Spec: §6 — WaitSet-Conditions über Reader-State (SampleStateKind, ViewStateKind, InstanceStateKind), SQL92-Filter- Expression für QueryCondition.

Repo: crates/py/src/conditions.rs (~135 Zeilen) — zwei PyClasses PyReadCondition, PyQueryCondition über den Rust-Conditions aus crates/dcps/src/condition.rs. SampleState/ViewState/ InstanceState-Bitmask-Konstanten in ffi.rs exportiert als SAMPLE_STATE_*, VIEW_STATE_*, INSTANCE_STATE_* Modul- Attribute; pure-Python-Wrapper-Module crates/py/python/zerodds/{sample_state,view_state,instance_state}.py liefern den pythonischen Namespace. PyWaitSet::attach_read_condition / attach_query_condition in ffi.rs.

Tests: crates/py/python/tests/test_read_conditions.py (11 Tests): test_sample_state_constants, test_view_state_constants, test_instance_state_constants, test_read_condition_constructible, test_read_condition_modes, test_read_condition_unknown_mode_raises, test_read_condition_attaches_to_waitset, test_query_condition_constructible_simple_filter, test_query_condition_with_parameters, test_query_condition_invalid_sql_raises, test_query_condition_attaches_to_waitset.

Status: done

§6.7 sphinx-Doc-Pfad

Spec: §6 — crates/py/docs/ mit API-Inventarisierung aus _core über autodoc.

Repo: crates/py/docs/conf.py mit autodoc/napoleon/viewcode/ intersphinx extensions, autodoc_default_options und autodoc_mock_imports = ["zerodds._core"] für RTD-Build ohne maturin. crates/py/docs/api.rst listet voll alle PyClasses und pure-Python-Module mit autoclass/automodule.

Tests: PYTHONPATH=crates/py/python python3 -m sphinx -b html crates/py/docs /tmp/zd-py-sphinx produziert HTML mit allen Modulen (zerodds._core, zerodds.cdr, zerodds.idl, zerodds.loader) und allen 13 PyClasses, 0 hard errors (14 warnings = fehlende docstrings im Mock-Pfad).

Status: done

§7 Stabilität

Spec: §7 — semver-Politik, v1.0 stabil, Major-Bump für Breaking-Changes.

Repo: crates/py/Cargo.toml (version.workspace = true1.0.0-rc.3 aus dem Workspace-Cargo).

Tests: kein eigener Stabilitäts-Test (Versionsabgleich-CI in tests/system/ falls vorhanden).

Status: n/a (informative) — Policy-Statement, nicht test-pflichtig.

§8 Lizenz

Spec: §8 — Apache-2.0 (Workspace-Default).

Repo: crates/py/Cargo.toml (license.workspace = true), LICENSE-APACHE im Repo-Root.

Tests:

Status: n/a (informative).

§9 Referenzen

Spec: §9 — Liste von externen Specs (OMG DDS 1.4, XTypes 1.3, PSM-Cxx 1.0) und internen Vendor-Specs (zerodds-c-api-1.0, zerodds-ffi-loader-1.0, zerodds-java-omgdds-1.0) plus PyO3 0.22 und PEP 384.

Repo: Verweise verlinkt zu den jeweiligen docs/specs/*.md-Dateien und externen Quellen.

Tests:

Status: n/a (informative).


Audit-Status

42 done / 0 partial / 0 open / 5 n/a (informative) / 0 n/a (rejected).

Test-Läufe:

  • Lokal (macOS, Apple Silicon): cd crates/py && SDKROOT=$(xcrun --show-sdk-path) maturin develop --features extension-module && python3 -m pytest python/tests/ — 96 Tests grün, 9 Conditional-Skip.
  • Bench-Host Linux-Bench-Host (Debian 12, x86_64, Cyclone DDS 0.10.2): identischer pytest-Aufruf, plus ZERODDS_LIB=$HOME/zerodds/target/debug/libzerodds.so und LD_LIBRARY_PATH=$HOME/zerodds/target/debug102 Tests grün, 3 Conditional-Skip (ROS-2 fehlt + 1 live-E2E-Placeholder).

Auf dem Linux-Bench-Host laufen die loader-smoke-Suite (libzerodds.so verfügbar), Multi-Process-Subprocess-Roundtrip und Cross-Vendor-SPDP-Discovery gegen Cyclone ddsperf durch.

Keine offenen Punkte.