Skip to content
This repository was archived by the owner on Feb 8, 2026. It is now read-only.

Commit 80c287c

Browse files
authored
remove ModelVariables class, update dimension, uprev to 0.3.0rc0 (#7)
1 parent 5dbf1c5 commit 80c287c

8 files changed

Lines changed: 198 additions & 89 deletions

File tree

.bumpversion.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tool.bumpversion]
2-
current_version = "0.2.1rc0"
2+
current_version = "0.3.0rc0"
33
parse = """(?x)
44
(?P<major>0|[1-9]\\d*)\\.
55
(?P<minor>0|[1-9]\\d*)\\.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mdreader"
3-
version = "0.2.1rc0"
3+
version = "0.3.0rc0"
44
description = "A Python library for reading and parsing Functional Mock-up Interface model description XML file."
55
readme = "README.md"
66
authors = [

src/mdreader/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.1rc0"
1+
__version__ = "0.3.0rc0"

src/mdreader/fmi2.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
"StringVariable",
4545
"EnumerationVariable",
4646
"ScalarVariable",
47-
"ModelVariables",
4847
"UnitDefinitions",
4948
"TypeDefinitions",
5049
"FmiModelDescription",
@@ -1489,22 +1488,6 @@ def to_xml(self) -> Element:
14891488
return element
14901489

14911490

1492-
class ModelVariables(BaseModel):
1493-
"""Model variables list"""
1494-
1495-
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
1496-
1497-
variables: Annotated[list[ScalarVariable], Field(..., alias="ScalarVariable")]
1498-
1499-
def to_xml(self) -> Element:
1500-
"""Convert ModelVariables to XML Element"""
1501-
element = Element("ModelVariables")
1502-
if self.variables is not None:
1503-
for variable in self.variables:
1504-
element.append(variable.to_xml())
1505-
return element
1506-
1507-
15081491
class UnitDefinitions(BaseModel):
15091492
"""Unit definitions list"""
15101493

@@ -1696,7 +1679,7 @@ class FmiModelDescription(BaseModel):
16961679
),
16971680
] = None
16981681
model_variables: Annotated[
1699-
ModelVariables,
1682+
list[ScalarVariable],
17001683
Field(
17011684
...,
17021685
alias="ModelVariables",
@@ -1758,7 +1741,10 @@ def to_xml(self) -> Element:
17581741
if self.vendor_annotations is not None:
17591742
element.append(self.vendor_annotations.to_xml())
17601743
if self.model_variables is not None:
1761-
element.append(self.model_variables.to_xml())
1744+
model_vars_elem = Element("ModelVariables")
1745+
for variable in self.model_variables:
1746+
model_vars_elem.append(variable.to_xml())
1747+
element.append(model_vars_elem)
17621748
if self.model_structure is not None:
17631749
element.append(self.model_structure.to_xml())
17641750

@@ -2213,13 +2199,13 @@ def _parse_vendor_annotations(elem: Element) -> Annotation:
22132199
return Annotation(tools=tools)
22142200

22152201

2216-
def _parse_model_variables(elem: Element) -> ModelVariables:
2217-
"""Parse ModelVariables element"""
2202+
def _parse_model_variables(elem: Element) -> list[ScalarVariable]:
2203+
"""Parse ModelVariables element into a flat list"""
22182204
variables = []
22192205
for variable_elem in elem.findall("ScalarVariable"):
22202206
variable = _parse_scalar_variable(variable_elem)
22212207
variables.append(variable)
2222-
return ModelVariables(variables=variables)
2208+
return variables
22232209

22242210

22252211
def _parse_scalar_variable(elem: Element) -> ScalarVariable:

src/mdreader/fmi3.py

Lines changed: 105 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
"EnumerationVariable",
6666
"ClockVariable",
6767
"Variable",
68-
"ModelVariables",
6968
"UnitDefinitions",
7069
"TypeDefinitions",
7170
"FmiModelDescription",
@@ -786,7 +785,7 @@ class Annotation(BaseModel):
786785

787786
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
788787

789-
tools: Annotated[list[Tool], Field(..., alias="Tool")]
788+
tools: Annotated[list[Tool] | None, Field(default=None, alias="Tool")] = None
790789

791790
def to_xml(self) -> Element:
792791
"""Convert Annotation to XML Element"""
@@ -802,11 +801,20 @@ class Dimension(BaseModel):
802801

803802
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
804803

805-
start: Annotated[int | None, Field(default=None, alias="start")]
804+
start: Annotated[int | None, Field(default=None, alias="start")] = None
806805
value_reference: Annotated[
807806
int | None, Field(default=None, alias="valueReference")
808807
] = None
809808

809+
@model_validator(mode="after")
810+
def _validate_exclusive_start_or_value_reference(self) -> "Dimension":
811+
"""Ensure exactly one of start or value_reference is provided."""
812+
if (self.start is None) == (self.value_reference is None):
813+
raise ValueError(
814+
"Dimension: exactly one of start or valueReference must be set"
815+
)
816+
return self
817+
810818
def to_xml(self) -> Element:
811819
"""Convert Dimension to XML Element"""
812820
element = Element("Dimension")
@@ -3113,7 +3121,7 @@ class Variable(BaseModel):
31133121
Field(default=None, alias="Clock", description="Clock variable definition"),
31143122
] = None
31153123

3116-
def get_variable_type(self):
3124+
def get_variable_type(self) -> str | None:
31173125
"""Get the type of variable based on which field is set"""
31183126
if self.float32 is not None:
31193127
return "Float32"
@@ -3147,6 +3155,91 @@ def get_variable_type(self):
31473155
return "Clock"
31483156
return None
31493157

3158+
def _concrete(
3159+
self,
3160+
) -> (
3161+
Float32Variable
3162+
| Float64Variable
3163+
| Int8Variable
3164+
| Int16Variable
3165+
| Int32Variable
3166+
| Int64Variable
3167+
| UInt8Variable
3168+
| UInt16Variable
3169+
| UInt32Variable
3170+
| UInt64Variable
3171+
| BooleanVariable
3172+
| StringVariable
3173+
| BinaryVariable
3174+
| EnumerationVariable
3175+
| ClockVariable
3176+
):
3177+
"""Return the underlying concrete variable instance or raise if unset."""
3178+
for attr in (
3179+
"float32",
3180+
"float64",
3181+
"int8",
3182+
"int16",
3183+
"int32",
3184+
"int64",
3185+
"uint8",
3186+
"uint16",
3187+
"uint32",
3188+
"uint64",
3189+
"boolean",
3190+
"string",
3191+
"binary",
3192+
"enumeration",
3193+
"clock",
3194+
):
3195+
value = getattr(self, attr)
3196+
if value is not None:
3197+
return value
3198+
raise ValueError("No variable type is set")
3199+
3200+
@property
3201+
def concrete(
3202+
self,
3203+
) -> (
3204+
Float32Variable
3205+
| Float64Variable
3206+
| Int8Variable
3207+
| Int16Variable
3208+
| Int32Variable
3209+
| Int64Variable
3210+
| UInt8Variable
3211+
| UInt16Variable
3212+
| UInt32Variable
3213+
| UInt64Variable
3214+
| BooleanVariable
3215+
| StringVariable
3216+
| BinaryVariable
3217+
| EnumerationVariable
3218+
| ClockVariable
3219+
):
3220+
"""Direct access to the concrete variable instance."""
3221+
return self._concrete()
3222+
3223+
@property
3224+
def name(self) -> str:
3225+
return self.concrete.name
3226+
3227+
@property
3228+
def value_reference(self) -> int:
3229+
return self.concrete.value_reference
3230+
3231+
@property
3232+
def causality(self) -> CausalityEnum | None:
3233+
return getattr(self.concrete, "causality", None)
3234+
3235+
@property
3236+
def variability(self) -> VariabilityEnum | None:
3237+
return getattr(self.concrete, "variability", None)
3238+
3239+
@property
3240+
def initial(self) -> InitialEnum | None:
3241+
return getattr(self.concrete, "initial", None)
3242+
31503243
def to_xml(self) -> Element:
31513244
"""Convert Variable to XML Element"""
31523245
# Add the appropriate variable type element
@@ -3184,22 +3277,6 @@ def to_xml(self) -> Element:
31843277
raise ValueError("No variable type is set")
31853278

31863279

3187-
class ModelVariables(BaseModel):
3188-
"""Model variables list for FMI 3.0"""
3189-
3190-
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
3191-
3192-
variables: Annotated[list[Variable], Field(..., alias="Variable")]
3193-
3194-
def to_xml(self) -> Element:
3195-
"""Convert ModelVariables to XML Element"""
3196-
element = Element("ModelVariables")
3197-
if self.variables is not None:
3198-
for variable in self.variables:
3199-
element.append(variable.to_xml())
3200-
return element
3201-
3202-
32033280
class UnitDefinitions(BaseModel):
32043281
"""Unit definitions list for FMI 3.0"""
32053282

@@ -3391,7 +3468,7 @@ class FmiModelDescription(BaseModel):
33913468
),
33923469
] = None
33933470
model_variables: Annotated[
3394-
ModelVariables,
3471+
list[Variable],
33953472
Field(
33963473
...,
33973474
alias="ModelVariables",
@@ -3453,7 +3530,10 @@ def to_xml(self) -> Element:
34533530
if self.vendor_annotations is not None:
34543531
element.append(self.vendor_annotations.to_xml())
34553532
if self.model_variables is not None:
3456-
element.append(self.model_variables.to_xml())
3533+
model_vars_elem = Element("ModelVariables")
3534+
for variable in self.model_variables:
3535+
model_vars_elem.append(variable.to_xml())
3536+
element.append(model_vars_elem)
34573537
if self.model_structure is not None:
34583538
element.append(self.model_structure.to_xml())
34593539

@@ -5182,8 +5262,8 @@ def _parse_dimensions(elem: Element) -> list[Dimension] | None:
51825262
return dims or None
51835263

51845264

5185-
def _parse_model_variables(elem: Element) -> ModelVariables:
5186-
"""Parse ModelVariables element"""
5265+
def _parse_model_variables(elem: Element) -> list[Variable]:
5266+
"""Parse ModelVariables element into a flat list"""
51875267
variables = []
51885268
# Find all variable types in the model variables section
51895269
for var_type in [
@@ -5206,7 +5286,7 @@ def _parse_model_variables(elem: Element) -> ModelVariables:
52065286
for variable_elem in elem.findall(var_type):
52075287
variable = _parse_variable(variable_elem, var_type)
52085288
variables.append(variable)
5209-
return ModelVariables(variables=variables)
5289+
return variables
52105290

52115291

52125292
def _parse_variable(elem: Element, var_type: str) -> Variable:

tests/test_fmi2.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,8 @@ def test_scarlar_variables(
175175
):
176176
filename = (reference_fmus_dir / reference_fmu).absolute()
177177
md = read_model_description(filename)
178-
input_vars = [
179-
var.name for var in md.model_variables.variables if var.causality == "input"
180-
]
181-
output_vars = [
182-
var.name for var in md.model_variables.variables if var.causality == "output"
183-
]
178+
input_vars = [var.name for var in md.model_variables if var.causality == "input"]
179+
output_vars = [var.name for var in md.model_variables if var.causality == "output"]
184180
assert sorted(input_vars) == sorted(expected_inputs)
185181
assert sorted(output_vars) == sorted(expected_outputs)
186182

@@ -411,7 +407,7 @@ def test_variable_properties(reference_fmu, expected_variables, reference_fmus_d
411407
md = read_model_description(filename)
412408

413409
# Create a mapping of variable names to variables for easy lookup
414-
var_map = {var.name: var for var in md.model_variables.variables}
410+
var_map = {var.name: var for var in md.model_variables}
415411

416412
for expected_var in expected_variables:
417413
var_name = expected_var["name"]
@@ -877,7 +873,6 @@ def test_xml_serialization():
877873
"""Test the to_xml() methods for all model classes"""
878874
from mdreader.fmi2 import (
879875
FmiModelDescription,
880-
ModelVariables,
881876
ScalarVariable,
882877
RealVariable,
883878
CausalityEnum,
@@ -1165,11 +1160,8 @@ def test_xml_serialization():
11651160
assert scalar_var_xml.get("variability") == "continuous"
11661161
assert len(scalar_var_xml) == 2 # Real element and Annotations element
11671162

1168-
# Test ModelVariables to_xml
1169-
model_vars = ModelVariables(variables=[scalar_var])
1170-
model_vars_xml = model_vars.to_xml()
1171-
assert model_vars_xml.tag == "ModelVariables"
1172-
assert len(model_vars_xml) == 1
1163+
# Collect model variables as a flat list
1164+
model_vars = [scalar_var]
11731165

11741166
# Test UnitDefinitions to_xml
11751167
from mdreader.fmi2 import UnitDefinitions
@@ -1221,7 +1213,6 @@ def test_xml_serialization_roundtrip():
12211213
"""Test that XML serialization and deserialization work correctly"""
12221214
from mdreader.fmi2 import (
12231215
FmiModelDescription,
1224-
ModelVariables,
12251216
ScalarVariable,
12261217
RealVariable,
12271218
CausalityEnum,
@@ -1239,12 +1230,11 @@ def test_xml_serialization_roundtrip():
12391230
variability=VariabilityEnum.continuous,
12401231
real=real_var,
12411232
)
1242-
model_vars = ModelVariables(variables=[scalar_var])
12431233
model_desc = FmiModelDescription(
12441234
fmi_version="2.0",
12451235
model_name="TestModel",
12461236
guid="{12345678-1234-5678-9012-123456789012}",
1247-
model_variables=model_vars,
1237+
model_variables=[scalar_var],
12481238
)
12491239

12501240
# Convert to XML
@@ -1309,14 +1299,12 @@ def test_xml_serialization_roundtrip_with_reference_fmus(
13091299
)
13101300

13111301
# Compare variable counts
1312-
assert len(original_md.model_variables.variables) == len(
1313-
reconstructed_md.model_variables.variables
1314-
)
1302+
assert len(original_md.model_variables) == len(reconstructed_md.model_variables)
13151303

13161304
# Compare some variable properties
13171305
for orig_var, recon_var in zip(
1318-
original_md.model_variables.variables,
1319-
reconstructed_md.model_variables.variables,
1306+
original_md.model_variables,
1307+
reconstructed_md.model_variables,
13201308
):
13211309
assert orig_var.name == recon_var.name
13221310
assert orig_var.value_reference == recon_var.value_reference

0 commit comments

Comments
 (0)