From 6ed16fd82d54ea8c1b6873ff6562a318391d2334 Mon Sep 17 00:00:00 2001 From: Daniel Thaler Date: Thu, 31 Aug 2023 23:20:32 +0200 Subject: [PATCH] enable access to more information from the specification --- autosar_data.pyi | 45 +++++++++ src/lib.rs | 119 ++++++++++++---------- src/specification.rs | 201 +++++++++++++++++++++++++++++++++++++ test/element_test.py | 8 +- test/specification_test.py | 112 +++++++++++++++++++++ test/test.py | 2 + 6 files changed, 432 insertions(+), 55 deletions(-) create mode 100644 src/specification.rs create mode 100644 test/specification_test.py diff --git a/autosar_data.pyi b/autosar_data.pyi index e80771f..d7f501c 100644 --- a/autosar_data.pyi +++ b/autosar_data.pyi @@ -27,6 +27,7 @@ EnumItem: TypeAlias = str # ~2500 variants is too many to list here CharacterData: TypeAlias = Union[EnumItem, str, int, float] ElementContent: TypeAlias = Union[Element, CharacterData] VersionSpecification: TypeAlias = Union[AutosarVersion, List[AutosarVersion]] +CharacterDataType: TypeAlias = Union[CharacterDataTypeEnum, CharacterDataTypeFloat, CharacterDataTypeRestrictedString, CharacterDataTypeString, CharacterDataTypeUnsignedInt] class ArxmlFile: """ @@ -303,6 +304,13 @@ class ElementType: def find_sub_element(self, target_name: ElementName, version: VersionSpecification) -> ElementType: """find the ElementType of the named sub element in the specification of this ElementType""" ... + chardata_spec: CharacterDataType + """the specification of the character data content of elements of this type""" + attributes_spec: List[AttributeSpec] + """a list of the specifications of all attributes allowed on elements of this type""" + def find_attribute_spec(self, attribute_name: AttributeName) -> AttributeSpec: + """find the specification for the given attribute name""" + ... class ElementsDfsIterator: """ @@ -368,6 +376,43 @@ class ValidSubElementInfo: is_allowed: bool """is the sub element currently allowed, given the existing content of the element. Note that some sub elements are mutually exclusive""" +class AttributeSpec: + """The specification of an attribute""" + attribute_name: str + """name of the attribute""" + value_spec: CharacterDataType + """specification of the attribute value""" + required: bool + """is the attribute required or optional""" + +class CharacterDataTypeEnum: + """Character data type: enum""" + values: List[str] + """List of valid enum values""" + +class CharacterDataTypeFloat: + """Character data type: float""" + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + +class CharacterDataTypeRestrictedString: + """Character data type: restricted string""" + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + regex: str + """to be valid, a string must match this regex""" + +class CharacterDataTypeString: + """Character data type: string""" + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + +class CharacterDataTypeUnsignedInt: + """Character data type: unsigned int""" + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + + __version__: str """ Version of the running autosar_data module. diff --git a/src/lib.rs b/src/lib.rs index 8afe95d..2414834 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod arxmlfile; mod element; mod model; mod version; +mod specification; use version::*; @@ -122,6 +123,63 @@ enum ContentType { Mixed, } +#[pyclass] +#[derive(Debug)] +/// Specification of an attribute +struct AttributeSpec { + #[pyo3(get)] + /// name of the attribute + attribute_name: String, + /// specification of the attribute value + value_spec: &'static CharacterDataSpec, + #[pyo3(get)] + /// is the attribute required or optional + required: bool, +} + +#[pyclass] +#[derive(Debug)] +/// The character data in an element or attribute is an enum value +struct CharacterDataTypeEnum { + #[pyo3(get)] + /// list of permitted enum values + values: Vec, +} + +#[pyclass] +#[derive(Debug)] +/// The character data in an element or attribute is a string that must match a regex +struct CharacterDataTypeRestrictedString { + #[pyo3(get)] + /// validation regex + regex: String, + #[pyo3(get)] + /// max length (if any) + max_length: Option, +} + +#[pyclass] +#[derive(Debug)] +/// The character data in an element or attribute is a string +struct CharacterDataTypeString { + #[pyo3(get)] + /// does this element preserve whitespace in its character data + preserve_whitespace: bool, + #[pyo3(get)] + /// max length (if any) + max_length: Option, +} + +#[pyclass] +#[derive(Debug)] +/// The character data in an element or attribute is an unsigned integer +struct CharacterDataTypeUnsignedInt(()); + +#[pyclass] +#[derive(Debug)] +/// The character data in an element or attribute is a float +struct CharacterDataTypeFloat(()); + #[pymethods] impl IncompatibleAttributeError { fn __repr__(&self) -> String { @@ -194,60 +252,6 @@ impl IncompatibleElementError { } } -#[pymethods] -impl ElementType { - fn __repr__(&self) -> String { - format!("{:#?}", self.0) - } - - #[getter] - fn is_named(&self) -> bool { - self.0.is_named() - } - - #[getter] - fn is_ref(&self) -> bool { - self.0.is_ref() - } - - #[getter] - fn is_ordered(&self) -> bool { - self.0.is_ordered() - } - - #[getter] - fn splittable(&self) -> Vec { - let versions = expand_version_mask(self.0.splittable()); - versions - .iter() - .map(|&ver| AutosarVersion::from(ver)) - .collect() - } - - fn splittable_in(&self, version: AutosarVersion) -> bool { - self.0.splittable_in(version.into()) - } - - fn reference_dest_value(&self, target: &ElementType) -> Option { - self.0 - .reference_dest_value(&target.0) - .map(|enumitem| enumitem.to_string()) - } - - fn find_sub_element( - &self, - target_name: String, - version_obj: PyObject, - ) -> PyResult> { - let version = version_mask_from_any(version_obj)?; - let elem_name = get_element_name(target_name)?; - Ok(self - .0 - .find_sub_element(elem_name, version) - .map(|(etype, _)| ElementType(etype))) - } -} - #[pymethods] impl ContentType { fn __repr__(&self) -> String { @@ -367,6 +371,7 @@ impl ValidSubElementInfo { } } + /// Provides functionality to read, modify and write Autosar arxml files, /// both separately and in projects consisting of multiple files. /// @@ -399,7 +404,13 @@ fn autosar_data(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add("AutosarDataError", py.get_type::())?; m.add("__version__", intern!(m.py(), env!("CARGO_PKG_VERSION")))?; Ok(()) diff --git a/src/specification.rs b/src/specification.rs new file mode 100644 index 0000000..6764012 --- /dev/null +++ b/src/specification.rs @@ -0,0 +1,201 @@ +use crate::*; + +#[pymethods] +impl ElementType { + fn __repr__(&self) -> String { + format!("{:#?}", self.0) + } + + #[getter] + fn is_named(&self) -> bool { + self.0.is_named() + } + + #[getter] + fn is_ref(&self) -> bool { + self.0.is_ref() + } + + #[getter] + fn is_ordered(&self) -> bool { + self.0.is_ordered() + } + + #[getter] + fn splittable(&self) -> Vec { + let versions = expand_version_mask(self.0.splittable()); + versions + .iter() + .map(|&ver| AutosarVersion::from(ver)) + .collect() + } + + fn splittable_in(&self, version: AutosarVersion) -> bool { + self.0.splittable_in(version.into()) + } + + fn reference_dest_value(&self, target: &ElementType) -> Option { + self.0 + .reference_dest_value(&target.0) + .map(|enumitem| enumitem.to_string()) + } + + fn find_sub_element( + &self, + target_name: String, + version_obj: PyObject, + ) -> PyResult> { + let version = version_mask_from_any(version_obj)?; + let elem_name = get_element_name(target_name)?; + Ok(self + .0 + .find_sub_element(elem_name, version) + .map(|(etype, _)| ElementType(etype))) + } + + #[getter] + fn chardata_spec(&self) -> Option { + self.0.chardata_spec().map(character_data_spec_to_object) + } + + #[getter] + fn attributes_spec(&self) -> Vec { + self.0 + .attribute_spec_iter() + .map(|(attribute_name, value_spec, required)| AttributeSpec { + attribute_name: attribute_name.to_string(), + value_spec, + required, + }) + .collect() + } + + fn find_attribute_spec(&self, attrname_str: String) -> PyResult { + let attrname = + autosar_data_specification::AttributeName::from_str(&attrname_str).map_err(|_| { + pyo3::exceptions::PyTypeError::new_err(format!( + "'{attrname_str}' cannot be converted to 'AttributeName'" + )) + })?; + + if let Some(attrspec) = self.0.find_attribute_spec(attrname) { + Ok(AttributeSpec { + attribute_name: attrname_str.to_owned(), + value_spec: attrspec.spec, + required: attrspec.required, + }) + } else { + Err(pyo3::exceptions::PyValueError::new_err(format!("'{attrname_str}' is not a valid attribute for this ElementType"))) + } + } +} + +#[pymethods] +impl AttributeSpec { + fn __repr__(&self) -> String { + format!("{:#?}", self) + } + + #[getter] + fn value_spec(&self) -> PyObject { + character_data_spec_to_object(self.value_spec) + } +} + +#[pymethods] +impl CharacterDataTypeEnum { + fn __repr__(&self) -> String { + format!("{:?}", self) + } + + fn __str__(&self) -> String { + format!("CharacterDataType: Enum of [{}]", self.values.join(", ")) + } +} + +#[pymethods] +impl CharacterDataTypeFloat { + fn __repr__(&self) -> String { + format!("{:?}", self) + } + + fn __str__(&self) -> String { + "CharacterDataType: Float".to_owned() + } +} + +#[pymethods] +impl CharacterDataTypeRestrictedString { + fn __repr__(&self) -> String { + format!("{:?}", self) + } + + fn __str__(&self) -> String { + format!("CharacterDataType: String matching \"{}\"", self.regex) + } +} + +#[pymethods] +impl CharacterDataTypeString { + fn __repr__(&self) -> String { + format!("{:?}", self) + } + + fn __str__(&self) -> String { + "CharacterDataType: String".to_owned() + } +} + +#[pymethods] +impl CharacterDataTypeUnsignedInt { + fn __repr__(&self) -> String { + format!("{:?}", self) + } + + fn __str__(&self) -> String { + "CharacterDataType: Float".to_owned() + } +} + +fn character_data_spec_to_object(spec: &CharacterDataSpec) -> PyObject { + Python::with_gil(|py| match spec { + CharacterDataSpec::Enum { items } => { + let pytype = Py::new( + py, + CharacterDataTypeEnum { + values: items.iter().map(|(item, _)| item.to_string()).collect(), + }, + ); + pytype.unwrap().into_py(py) + } + CharacterDataSpec::Pattern { + regex, max_length, .. + } => { + let pytype = Py::new( + py, + CharacterDataTypeRestrictedString { + regex: regex.to_string(), + max_length: *max_length, + }, + ); + pytype.unwrap().into_py(py) + } + CharacterDataSpec::String { + preserve_whitespace, + max_length, + } => { + let pytype = Py::new( + py, + CharacterDataTypeString { + preserve_whitespace: *preserve_whitespace, + max_length: *max_length, + }, + ); + pytype.unwrap().into_py(py) + } + CharacterDataSpec::UnsignedInteger => Py::new(py, CharacterDataTypeUnsignedInt(())) + .unwrap() + .into_py(py), + CharacterDataSpec::Double => Py::new(py, CharacterDataTypeFloat(())).unwrap().into_py(py), + }) +} diff --git a/test/element_test.py b/test/element_test.py index b347609..0c8b30e 100644 --- a/test/element_test.py +++ b/test/element_test.py @@ -469,6 +469,8 @@ def test_element_attributes() -> None: assert len([attr for attr in el_autosar.attributes]) == 5 el_autosar.remove_attribute("T") assert len([attr for attr in el_autosar.attributes]) == 4 + with pytest.raises(AutosarDataError): + el_autosar.remove_attribute("xyz") def test_file_membership() -> None: @@ -477,7 +479,7 @@ def test_file_membership() -> None: file2 = model.create_file("file2", AutosarVersion.AUTOSAR_00050) el_ar_packages = model.root_element.create_sub_element("AR-PACKAGES") el_pkg1 = el_ar_packages.create_named_sub_element("AR-PACKAGE", "Pkg1") - el_pkg1.create_sub_element("ELEMENTS") + el_elements = el_pkg1.create_sub_element("ELEMENTS") el_pkg2 = el_ar_packages.create_named_sub_element("AR-PACKAGE", "Pkg2") total_element_count = len([e for e in model.elements_dfs]) @@ -499,6 +501,10 @@ def test_file_membership() -> None: el_pkg1.add_to_file(file2) assert file2 in el_pkg1.file_membership[1] + el_pkg1.remove_sub_element(el_elements) + # with pytest.raises(AutosarDataError): + # el_elements.remove_from_file(file2) + def test_element_misc() -> None: model = AutosarModel() diff --git a/test/specification_test.py b/test/specification_test.py new file mode 100644 index 0000000..9b17344 --- /dev/null +++ b/test/specification_test.py @@ -0,0 +1,112 @@ +from autosar_data import * +import pytest + +def test_specification_basic() -> None: + model = AutosarModel() + assert model.root_element.element_type.chardata_spec is None + + el_ar_packages = model.root_element.create_sub_element("AR-PACKAGES") + el_ar_package = el_ar_packages.create_named_sub_element("AR-PACKAGE", "Pkg1") + + # get a character data specification + el_short_name = el_ar_package.get_sub_element("SHORT-NAME") + # SHORT-NAME contains a restricted string + assert isinstance(el_short_name.element_type.chardata_spec, CharacterDataTypeRestrictedString) + + attribute_spec = model.root_element.element_type.attributes_spec + assert len(attribute_spec) > 3 + assert not attribute_spec[0].__str__() is None + assert not attribute_spec[0].__repr__() is None + + with pytest.raises(ValueError): + model.root_element.element_type.find_attribute_spec("DEST") + with pytest.raises(TypeError): + model.root_element.element_type.find_attribute_spec("xyz") + + +def test_specification_enum() -> None: + model = AutosarModel() + el_ar_packages = model.root_element.create_sub_element("AR-PACKAGES") + el_elements = el_ar_packages \ + .create_named_sub_element("AR-PACKAGE", "SysPkg") \ + .create_sub_element("ELEMENTS") + el_fibex_element_ref = el_elements \ + .create_named_sub_element("SYSTEM", "System") \ + .create_sub_element("FIBEX-ELEMENTS") \ + .create_sub_element("FIBEX-ELEMENT-REF-CONDITIONAL") \ + .create_sub_element("FIBEX-ELEMENT-REF") + dest_attr_spec = el_fibex_element_ref.element_type.find_attribute_spec("DEST") + print(dest_attr_spec.__repr__()) + assert dest_attr_spec.attribute_name == "DEST" + assert isinstance(dest_attr_spec, AttributeSpec) + assert isinstance(dest_attr_spec.value_spec, CharacterDataTypeEnum) + assert len(dest_attr_spec.value_spec.values) > 1 + assert not dest_attr_spec.__str__() is None + assert not dest_attr_spec.__repr__() is None + assert not dest_attr_spec.value_spec.__str__() is None + assert not dest_attr_spec.value_spec.__repr__() is None + + +def test_specification_float() -> None: + model = AutosarModel() + el_macrotick = model.root_element \ + .create_sub_element("AR-PACKAGES") \ + .create_named_sub_element("AR-PACKAGE", "pkg") \ + .create_sub_element("ELEMENTS") \ + .create_named_sub_element("FLEXRAY-CLUSTER", "fc") \ + .create_sub_element("FLEXRAY-CLUSTER-VARIANTS") \ + .create_sub_element("FLEXRAY-CLUSTER-CONDITIONAL") \ + .create_sub_element("MACROTICK-DURATION") + mtd_spec = el_macrotick.element_type.chardata_spec + assert isinstance(mtd_spec, CharacterDataTypeFloat) + assert not mtd_spec.__str__() is None + assert not mtd_spec.__repr__() is None + + +def test_specification_restricted_string() -> None: + model = AutosarModel() + el_ar_packages = model.root_element.create_sub_element("AR-PACKAGES") + el_elements = el_ar_packages \ + .create_named_sub_element("AR-PACKAGE", "SysPkg") \ + .create_sub_element("ELEMENTS") + el_fibex_element_ref = el_elements \ + .create_named_sub_element("SYSTEM", "System") \ + .create_sub_element("FIBEX-ELEMENTS") \ + .create_sub_element("FIBEX-ELEMENT-REF-CONDITIONAL") \ + .create_sub_element("FIBEX-ELEMENT-REF") + fibex_el_ref_spec = el_fibex_element_ref.element_type.chardata_spec + assert isinstance(fibex_el_ref_spec, CharacterDataTypeRestrictedString) + assert not fibex_el_ref_spec.regex is None + assert not fibex_el_ref_spec.__str__() is None + assert not fibex_el_ref_spec.__repr__() is None + + +def test_specification_string() -> None: + model = AutosarModel() + attr_s_spec = model.root_element.element_type.find_attribute_spec("S") + print(attr_s_spec) + assert isinstance(attr_s_spec, AttributeSpec) + assert isinstance(attr_s_spec.value_spec, CharacterDataTypeString) + assert not attr_s_spec.__str__() is None + assert not attr_s_spec.__repr__() is None + assert not attr_s_spec.value_spec.__str__() is None + assert not attr_s_spec.value_spec.__repr__() is None + + +def test_specification_uint() -> None: + model = AutosarModel() + el_cse_code = model.root_element \ + .create_sub_element("AR-PACKAGES") \ + .create_named_sub_element("AR-PACKAGE", "pkg") \ + .create_sub_element("ELEMENTS") \ + .create_named_sub_element("BSW-MODULE-TIMING", "bmt") \ + .create_sub_element("TIMING-GUARANTEES") \ + .create_named_sub_element("SYNCHRONIZATION-TIMING-CONSTRAINT", "stc") \ + .create_sub_element("TOLERANCE") \ + .create_sub_element("CSE-CODE") + cse_spec = el_cse_code.element_type.chardata_spec + assert isinstance(cse_spec, CharacterDataTypeUnsignedInt) + assert not cse_spec.__str__() is None + assert not cse_spec.__repr__() is None + + diff --git a/test/test.py b/test/test.py index 93fff14..b7e6814 100644 --- a/test/test.py +++ b/test/test.py @@ -20,6 +20,8 @@ def test_others() -> None: assert ar_pkg_type.splittable_in(AutosarVersion.AUTOSAR_00042) == True with pytest.raises(TypeError): model.root_element.element_type.find_sub_element("AR-PACKAGES", "wrong type") + with pytest.raises(TypeError): + model.root_element.element_type.find_sub_element("AR-PACKAGES", ["wrong type"]) assert AutosarVersion.AUTOSAR_4_0_1 in ar_pkg_type.splittable et_str = ar_pkg_type.__str__()