Repository: NTNU-IHB/PythonFMU

Issue:
Add PythonFMU version to modelDescription.xml
Version should be read from _version.py

Discussion:
markaren: Mhh.. the PyCharm formatter uses more indents :/ We should probably standardize on the formatiing rules.
markaren: It looks to be the `Continuation indent` setting is set to 8 in PyCharm, while you're formatter uses 6 as indent.
fcollonval: This is weird, the convention in Python is to use N * 4 spaces as indent convention. Where did you see a 6 spaces indent?
What I know to be different in PyCharm compare to black is the handling of long string and maybe the line length (black uses 88).

For reference the [black rules](https://black.readthedocs.io/en/stable/the_black_code_style.html)
markaren: In was wrong with 6 in black. I'ts 4
markaren: I can set the continuation indent to 4 in PyCharm. Merging this.

Patch:
diff --git a/pythonfmu/builder.py b/pythonfmu/builder.py
index 90a6b49..feb8fcf 100644
--- a/pythonfmu/builder.py
+++ b/pythonfmu/builder.py
@@ -53,7 +53,7 @@ class ModelDescriptionFetcher:
             instance = getattr(fmu_interface, class_name)("dummyInstance")
         finally:
             sys.path.remove(str(filepath.parent))  # remove inserted temporary path
-        
+
         if not isinstance(instance, Fmi2Slave):
             raise TypeError(
                 f"The provided class '{class_name}' does not inherit from {Fmi2Slave.__qualname__}"
@@ -88,7 +88,9 @@ class FmuBuilder:
         if documentation_folder is not None:
             documentation_folder = Path(documentation_folder)
             if not documentation_folder.exists():
-                raise ValueError(f"The documentation folder does not exists {documentation_folder!s}")
+                raise ValueError(
+                    f"The documentation folder does not exists {documentation_folder!s}"
+                )
 
         script_parent = script_file.resolve().parent.absolute()
         module_name = script_file.stem
@@ -159,7 +161,9 @@ class FmuBuilder:
                 binaries = Path("binaries")
                 src_binaries = HERE / "resources" / "binaries"
                 for f in itertools.chain(
-                    src_binaries.rglob("*.dll"), src_binaries.rglob("*.so"), src_binaries.rglob("*.dylib")
+                    src_binaries.rglob("*.dll"),
+                    src_binaries.rglob("*.so"),
+                    src_binaries.rglob("*.dylib"),
                 ):
                     relative_f = f.relative_to(src_binaries)
                     arcname = (
@@ -215,7 +219,10 @@ def main():
     )
 
     parser.add_argument(
-        "--doc", dest="documentation_folder", help="Documentation folder to include in the FMU.", default=None
+        "--doc",
+        dest="documentation_folder",
+        help="Documentation folder to include in the FMU.",
+        default=None,
     )
 
     # for option in FMI2_MODEL_OPTIONS:
diff --git a/pythonfmu/enums.py b/pythonfmu/enums.py
index 2cdece7..3557150 100644
--- a/pythonfmu/enums.py
+++ b/pythonfmu/enums.py
@@ -1,23 +1,24 @@
+"""FMI enumerations"""
 from enum import Enum
 
 
 class Fmi2Causality(Enum):
-    parameter = 0,
-    calculatedParameter = 1,
-    input = 2,
-    output = 3,
+    parameter = 0
+    calculatedParameter = 1
+    input = 2
+    output = 3
     local = 4
 
 
 class Fmi2Initial(Enum):
-    exact = 0,
-    approx = 1,
+    exact = 0
+    approx = 1
     calculated = 2
 
 
 class Fmi2Variability(Enum):
-    constant = 0,
-    fixed = 1,
-    tunable = 2,
-    discrete = 3,
+    constant = 0
+    fixed = 1
+    tunable = 2
+    discrete = 3
     continuous = 4
diff --git a/pythonfmu/fmi2slave.py b/pythonfmu/fmi2slave.py
index bdb89ad..b418e14 100644
--- a/pythonfmu/fmi2slave.py
+++ b/pythonfmu/fmi2slave.py
@@ -1,14 +1,16 @@
+"""Define the abstract facade class."""
 import datetime
 from abc import ABC, abstractmethod
-from collections import namedtuple, OrderedDict
+from collections import OrderedDict, namedtuple
 from typing import Any, ClassVar, Dict, List, Optional
 from uuid import uuid1
 from xml.etree.ElementTree import Element, SubElement
 
+from ._version import __version__ as VERSION
 from .enums import Fmi2Causality, Fmi2Initial, Fmi2Variability
 from .variables import Boolean, Integer, Real, ScalarVariable, String
 
-ModelOptions = namedtuple('ModelOptions', ['name', 'value', 'cli'])
+ModelOptions = namedtuple("ModelOptions", ["name", "value", "cli"])
 
 FMI2_MODEL_OPTIONS: List[ModelOptions] = [
     ModelOptions("needsExecutionTool", True, "no-external-tool"),
@@ -22,6 +24,7 @@ FMI2_MODEL_OPTIONS: List[ModelOptions] = [
 
 
 class Fmi2Slave(ABC):
+    """Abstract facade class to execute Python through FMI standard."""
 
     guid: ClassVar[str] = uuid1()
     author: ClassVar[Optional[str]] = None
@@ -48,52 +51,54 @@ class Fmi2Slave(ABC):
         """
 
         t = datetime.datetime.now(datetime.timezone.utc)
-        date_str = t.isoformat(timespec='seconds')
+        date_str = t.isoformat(timespec="seconds")
 
-        attrib=dict(
+        attrib = dict(
             fmiVersion="2.0",
             modelName=self.modelName,
             guid=f"{self.guid!s}",
-            generationTool="PythonFMU",
+            generationTool=f"PythonFMU {VERSION}",
             generationDateAndTime=date_str,
-            variableNamingConvention="structured"
+            variableNamingConvention="structured",
         )
         if self.description is not None:
-            attrib['description'] = self.description
+            attrib["description"] = self.description
         if self.author is not None:
-            attrib['author'] = self.author
+            attrib["author"] = self.author
         if self.license is not None:
-            attrib['license'] = self.license
+            attrib["license"] = self.license
         if self.version is not None:
-            attrib['version'] = self.version
+            attrib["version"] = self.version
         if self.copyright is not None:
-            attrib['copyright'] = self.copyright
+            attrib["copyright"] = self.copyright
 
-        root = Element('fmiModelDescription', attrib)
+        root = Element("fmiModelDescription", attrib)
 
         options = dict()
         for option in FMI2_MODEL_OPTIONS:
             value = model_options.get(option.name, option.value)
             v = "true" if value else "false"
             options[option.name] = v
-        options["modelIdentifier"]=self.modelName
+        options["modelIdentifier"] = self.modelName
 
-        SubElement(root, 'CoSimulation', attrib=options)
+        SubElement(root, "CoSimulation", attrib=options)
 
-        variables = SubElement(root, 'ModelVariables')
+        variables = SubElement(root, "ModelVariables")
         for v in self.vars.values():
             if ScalarVariable.requires_start(v):
                 self.__apply_start_value(v)
             variables.append(v.to_xml())
 
-        structure = SubElement(root, 'ModelStructure')
-        outputs = list(filter(lambda v: v.causality == Fmi2Causality.output, self.vars.values()))
-        
+        structure = SubElement(root, "ModelStructure")
+        outputs = list(
+            filter(lambda v: v.causality == Fmi2Causality.output, self.vars.values())
+        )
+
         if outputs:
-            outputs_node = SubElement(structure, 'Outputs')
+            outputs_node = SubElement(structure, "Outputs")
             for i, v in enumerate(self.vars.values()):
                 if v.causality == Fmi2Causality.output:
-                    SubElement(outputs_node, 'Unknown', attrib=dict(index=str(i+1)))
+                    SubElement(outputs_node, "Unknown", attrib=dict(index=str(i + 1)))
 
         return root
 
@@ -165,7 +170,9 @@ class Fmi2Slave(ABC):
             if isinstance(var, Integer):
                 refs.append(int(self.get_value(var.name)))
             else:
-                raise TypeError(f"Variable with valueReference={vr} is not of type Integer!")
+                raise TypeError(
+                    f"Variable with valueReference={vr} is not of type Integer!"
+                )
         return refs
 
     def get_real(self, vrs: List[int]) -> List[float]:
@@ -175,7 +182,9 @@ class Fmi2Slave(ABC):
             if isinstance(var, Real):
                 refs.append(float(self.get_value(var.name)))
             else:
-                raise TypeError(f"Variable with valueReference={vr} is not of type Real!")
+                raise TypeError(
+                    f"Variable with valueReference={vr} is not of type Real!"
+                )
         return refs
 
     def get_boolean(self, vrs: List[int]) -> List[bool]:
@@ -185,7 +194,9 @@ class Fmi2Slave(ABC):
             if isinstance(var, Boolean):
                 refs.append(bool(self.get_value(var.name)))
             else:
-                raise TypeError(f"Variable with valueReference={vr} is not of type Boolean!")
+                raise TypeError(
+                    f"Variable with valueReference={vr} is not of type Boolean!"
+                )
         return refs
 
     def get_string(self, vrs: List[int]) -> List[str]:
@@ -195,7 +206,9 @@ class Fmi2Slave(ABC):
             if isinstance(var, String):
                 refs.append(str(self.get_value(var.name)))
             else:
-                raise TypeError(f"Variable with valueReference={vr} is not of type String!")
+                raise TypeError(
+                    f"Variable with valueReference={vr} is not of type String!"
+                )
         return refs
 
     def set_integer(self, vrs: List[int], values: List[int]):
@@ -204,7 +217,9 @@ class Fmi2Slave(ABC):
             if isinstance(var, Integer):
                 self.set_value(var.name, value)
             else:
-                raise TypeError(f"Variable with valueReference={vr} is not of type Integer!")
+                raise TypeError(
+                    f"Variable with valueReference={vr} is not of type Integer!"
+                )
 
     def set_real(self, vrs: List[int], values: List[float]):
         for vr, value in zip(vrs, values):
@@ -212,7 +227,9 @@ class Fmi2Slave(ABC):
             if isinstance(var, Real):
                 self.set_value(var.name, value)
             else:
-                raise TypeError(f"Variable with valueReference={vr} is not of type Real!")
+                raise TypeError(
+                    f"Variable with valueReference={vr} is not of type Real!"
+                )
 
     def set_boolean(self, vrs: List[int], values: List[bool]):
         for vr, value in zip(vrs, values):
@@ -220,7 +237,9 @@ class Fmi2Slave(ABC):
             if isinstance(var, Boolean):
                 self.set_value(var.name, value)
             else:
-                raise TypeError(f"Variable with valueReference={vr} is not of type Boolean!")
+                raise TypeError(
+                    f"Variable with valueReference={vr} is not of type Boolean!"
+                )
 
     def set_string(self, vrs: List[int], values: List[str]):
         for vr, value in zip(vrs, values):
@@ -228,4 +247,6 @@ class Fmi2Slave(ABC):
             if isinstance(var, String):
                 self.set_value(var.name, value)
             else:
-                raise TypeError(f"Variable with valueReference={vr} is not of type String!")
+                raise TypeError(
+                    f"Variable with valueReference={vr} is not of type String!"
+                )
diff --git a/pythonfmu/variables.py b/pythonfmu/variables.py
index 1d169e8..741f752 100644
--- a/pythonfmu/variables.py
+++ b/pythonfmu/variables.py
@@ -1,3 +1,4 @@
+"""Classes describing interface variables."""
 from abc import ABC
 from enum import Enum
 from typing import Any, Optional
@@ -57,11 +58,13 @@ class ScalarVariable(ABC):
 
     @staticmethod
     def requires_start(v) -> bool:
-        return v.initial == Fmi2Initial.exact or \
-               v.initial == Fmi2Initial.approx or \
-               v.causality == Fmi2Causality.input or \
-               v.causality == Fmi2Causality.parameter or \
-               v.variability == Fmi2Variability.constant
+        return (
+            v.initial == Fmi2Initial.exact
+            or v.initial == Fmi2Initial.approx
+            or v.causality == Fmi2Causality.input
+            or v.causality == Fmi2Causality.parameter
+            or v.variability == Fmi2Variability.constant
+        )
 
     def to_xml(self) -> Element:
         attrib = dict()


Test patch:
diff --git a/pythonfmu/tests/test_fmi2slave.py b/pythonfmu/tests/test_fmi2slave.py
index 10c727d..814499a 100644
--- a/pythonfmu/tests/test_fmi2slave.py
+++ b/pythonfmu/tests/test_fmi2slave.py
@@ -1,6 +1,7 @@
 import pytest
 
 from pythonfmu import Fmi2Slave, Boolean, Integer, Real, String
+from pythonfmu import __version__ as VERSION
 
 # TODO test xml
 
@@ -27,6 +28,16 @@ def test_Fmi2Slave_constructor(model):
         _ = Slave("instanceSlave")
         assert Slave.modelName == model
 
+def test_Fmi2Slave_generation_tool():
+    class Slave(Fmi2Slave):
+        def do_step(self, t, dt):
+            return True
+    
+    slave = Slave("instance")
+    xml = slave.to_xml()
+
+    assert xml.attrib['generationTool'] == f"PythonFMU {VERSION}"
+
 @pytest.mark.parametrize("fmi_type", FMI2PY)
 @pytest.mark.parametrize("value", [
     False,