Repository: ebroecker/canmatrix (Python)

Issue:
Value Tables/Enums not exported when using sym.dump()
The sym dump method doesn't seem to actually save the enumerations into the output file if they aren't used by any signals within the matrix. 
I'm a bit confused by how the code is structured as it seems to expect the enums to all be saved in a gloabal variable called enums_dict, but it never actually populates it.. I cant quite see how this is intended to work. 
@ebroecker Do you know what the use of global variables here is for? 

Modifying this test shows that it doesn't work:
```
@pytest.mark.parametrize(
    'enum_str, enum_dict, enum_label',
    (
        ('enum Animal(0="Dog", 1="Cat", 2="Fox")', {"Animal": {0: "Dog", 1: "Cat", 2: "Fox"}}, "Simple enum"),
        ('''\
enum Animal(0="Dog", //A Comment
1="Cat",
2="Fox")''',
         {"Animal": {0: "Dog", 1: "Cat", 2: "Fox"}}, "Multiline enum"),
        ('enum Animal(0="Dog",1="Cat",2="Fox")', {"Animal": {0: "Dog", 1: "Cat", 2: "Fox"}}, "No Space  in Separator"),
    )
)
def test_enums_read(enum_str, enum_dict, enum_label):
        f = io.BytesIO('''\
FormatVersion=5.0 // Do not edit this line!
Title="An Example Title"

{{ENUMS}}
{}
'''.format(enum_str).encode('utf-8'),
    )

        matrix = canmatrix.formats.sym.load(f)
        assert matrix.load_errors == [], "Failed to load canmatrix, when testing enum case : '{}'".format(enum_label)
        assert matrix.value_tables == enum_dict, "Enum not parsed correctly : '{}'".format(enum_label)
        f_out = io.BytesIO()
        canmatrix.formats.sym.dump(matrix, f_out)
        f_in = io.BytesIO(f_out.getvalue())
        exported_sym_string = f_out.getvalue().decode('utf-8')
        new_matrix = canmatrix.formats.sym.load(f_in)
        assert matrix.value_tables == new_matrix.value_tables, "Enums not exported and reimported correctly"
```
(The last 4 lines have been added)

The fix seems like it could be simple enough to just build up the text from the value_table, but without knowing what the current code is trying to do I feel like there may be a better fix, or some refactoring required.


Discussion:
I think i have come up with a fix that removes the use of the global variables. I will make a PR and commit once I see whether my current PR is merged or needs fixing. (#479)
```
diff --git a/src/canmatrix/formats/sym.py b/src/canmatrix/formats/sym.py
index a67d137..4c46eb9 100644
--- a/src/canmatrix/formats/sym.py
+++ b/src/canmatrix/formats/sym.py
@@ -38,8 +38,6 @@ import canmatrix
 import canmatrix.utils
 
 logger = logging.getLogger(__name__)
-enum_dict = {}  # type: typing.Dict[str, str]
-enums = "{ENUMS}\n"
 
 
 def default_float_factory(value):  # type: (typing.Any) -> decimal.Decimal
@@ -101,8 +99,6 @@ def format_float(f):  # type: (typing.Any) -> str
 
 
 def create_signal(db, signal):  # type: (canmatrix.CanMatrix, canmatrix.Signal) -> str
-    global enums
-    global enum_dict
     output = ""
     output += "Var=%s " % signal.name
     if not signal.is_signed:
@@ -145,12 +141,7 @@ def create_signal(db, signal):  # type: (canmatrix.CanMatrix, canmatrix.Signal)
             val_tab_name = signal.name
 
         output += "/e:%s " % val_tab_name
-        if val_tab_name not in enum_dict:
-            enum_dict[val_tab_name] = "enum " + val_tab_name + "(" + ', '.join(
-                '%s="%s"' %
-                (key, val) for (
-                    key, val) in sorted(
-                    signal.values.items())) + ")"
+
 
     default = signal.initial_value  # type: ignore
     min_ok = signal.min is None or default >= signal.min
@@ -168,17 +159,31 @@ def create_signal(db, signal):  # type: (canmatrix.CanMatrix, canmatrix.Signal)
     output += "\n"
     return output
 
+def create_enum_from_signal_values(signal):
+    enum_dict = {}
+    if len(signal.values) > 0:
+        val_tab_name = signal.enumeration
+        if val_tab_name is None:
+            val_tab_name = signal.name
+
+        if val_tab_name not in enum_dict:
+            enum_dict[val_tab_name] = "enum " + val_tab_name + "(" + ', '.join(
+                '%s="%s"' %
+                (key, val) for (
+                    key, val) in sorted(
+                    signal.values.items())) + ")"
+    return enum_dict
 
 def dump(db, f, **options):  # type: (canmatrix.CanMatrix, typing.IO, **typing.Any) -> None
     """
     export canmatrix-object as .sym file (compatible to PEAK-Systems)
     """
-    global enum_dict
-    global enums
     sym_encoding = options.get('symExportEncoding', 'iso-8859-1')
     ignore_encoding_errors = options.get("ignoreExportEncodingErrors", "")
 
     enum_dict = {}
+    for enum_name, enum_values in db.value_tables.items():
+        enum_dict[enum_name] = "enum {}({})".format(enum_name, ', '.join('{}="{}"'.format(*items) for items in sorted(enum_values.items())))
     enums = "{ENUMS}\n"
 
     header = """\
@@ -294,6 +299,7 @@ Title=\"{}\"
                     output += "CycleTime=" + str(frame.effective_cycle_time) + "\n"
                 for signal in frame.signals:
                     output += create_signal(db, signal)
+                    enum_dict.update(create_enum_from_signal_values(signal))
                 output += "\n"
     enums += '\n'.join(sorted(enum_dict.values()))
     # write output file
diff --git a/src/canmatrix/tests/test_sym.py b/src/canmatrix/tests/test_sym.py
index d5b5ccd..c89b25b 100644
--- a/src/canmatrix/tests/test_sym.py
+++ b/src/canmatrix/tests/test_sym.py
@@ -211,10 +211,46 @@ Title="An Example Title"
 {{ENUMS}}
 {}
 '''.format(enum_str).encode('utf-8'),
-        )
+    )
 
         matrix = canmatrix.formats.sym.load(f)
         assert matrix.load_errors == [], "Failed to load canmatrix, when testing enum case : '{}'".format(enum_label)
         assert matrix.value_tables == enum_dict, "Enum not parsed correctly : '{}'".format(enum_label)
-        f_out = io.BytesIO()
-        canmatrix.formats.sym.dump(matrix, f_out)
+
+
+def test_enums_export():
+    f = io.BytesIO('''\
+FormatVersion=5.0 // Do not edit this line!
+Title="An Example Title"
+
+{ENUMS}
+enum Animal(0="Dog",1="Cat",2="Fox")
+
+{SENDRECEIVE}
+
+[Frame1]
+ID=000h
+DLC=8
+Var=Signal1 unsigned 0,16
+'''.encode('utf-8'),
+    )
+
+    matrix = canmatrix.formats.sym.load(f)
+    assert matrix.load_errors == [], "Failed to load canmatrix"
+
+    # Add an enum to Signal1
+    matrix.frame_by_name("Frame1").signal_by_name("Signal1").enumeration = "Plants"
+    matrix.frame_by_name("Frame1").signal_by_name("Signal1").values = {0: "Grass", 1: "Flower", 2: "Tree"}
+
+    # Export and reimport
+    f_out = io.BytesIO()
+    canmatrix.formats.sym.dump(matrix, f_out)
+    f_in = io.BytesIO(f_out.getvalue())
+    new_matrix = canmatrix.formats.sym.load(f_in)
+
+    # Check that Enums from Enums table exported and reimported correctly
+    assert new_matrix.value_tables["Animal"] == {0: "Dog", 1: "Cat", 2: "Fox"}
+
+    # Check that Enums from a Signal.Values property exported and reimported correctly
+    assert new_matrix.value_tables["Plants"] == {0: "Grass", 1: "Flower", 2: "Tree"}
+

```



Patch:
diff --git a/src/canmatrix/formats/sym.py b/src/canmatrix/formats/sym.py
index 75678647..b9557d5a 100644
--- a/src/canmatrix/formats/sym.py
+++ b/src/canmatrix/formats/sym.py
@@ -38,8 +38,6 @@
 import canmatrix.utils
 
 logger = logging.getLogger(__name__)
-enum_dict = {}  # type: typing.Dict[str, str]
-enums = "{ENUMS}\n"
 
 
 def default_float_factory(value):  # type: (typing.Any) -> decimal.Decimal
@@ -101,8 +99,6 @@ def format_float(f):  # type: (typing.Any) -> str
 
 
 def create_signal(db, signal):  # type: (canmatrix.CanMatrix, canmatrix.Signal) -> str
-    global enums
-    global enum_dict
     output = ""
     if sys.version_info > (3, 0):
         quote_name = not signal.name.isidentifier()
@@ -159,12 +155,7 @@ def create_signal(db, signal):  # type: (canmatrix.CanMatrix, canmatrix.Signal)
             val_tab_name = signal.name
 
         output += "/e:%s " % val_tab_name
-        if val_tab_name not in enum_dict:
-            enum_dict[val_tab_name] = "enum " + val_tab_name + "(" + ', '.join(
-                '%s="%s"' %
-                (key, val) for (
-                    key, val) in sorted(
-                    signal.values.items())) + ")"
+
 
     default = signal.initial_value  # type: ignore
     min_ok = signal.min is None or default >= signal.min
@@ -182,17 +173,31 @@ def create_signal(db, signal):  # type: (canmatrix.CanMatrix, canmatrix.Signal)
     output += "\n"
     return output
 
+def create_enum_from_signal_values(signal):
+    enum_dict = {}
+    if len(signal.values) > 0:
+        val_tab_name = signal.enumeration
+        if val_tab_name is None:
+            val_tab_name = signal.name
+
+        if val_tab_name not in enum_dict:
+            enum_dict[val_tab_name] = "enum " + val_tab_name + "(" + ', '.join(
+                '%s="%s"' %
+                (key, val) for (
+                    key, val) in sorted(
+                    signal.values.items())) + ")"
+    return enum_dict
 
 def dump(db, f, **options):  # type: (canmatrix.CanMatrix, typing.IO, **typing.Any) -> None
     """
     export canmatrix-object as .sym file (compatible to PEAK-Systems)
     """
-    global enum_dict
-    global enums
     sym_encoding = options.get('symExportEncoding', 'iso-8859-1')
     ignore_encoding_errors = options.get("ignoreExportEncodingErrors", "")
 
     enum_dict = {}
+    for enum_name, enum_values in db.value_tables.items():
+        enum_dict[enum_name] = "enum {}({})".format(enum_name, ', '.join('{}="{}"'.format(*items) for items in sorted(enum_values.items())))
     enums = "{ENUMS}\n"
 
     header = """\
@@ -308,6 +313,7 @@ def send_receive(for_frame):
                     output += "CycleTime=" + str(frame.effective_cycle_time) + "\n"
                 for signal in frame.signals:
                     output += create_signal(db, signal)
+                    enum_dict.update(create_enum_from_signal_values(signal))
                 output += "\n"
     enums += '\n'.join(sorted(enum_dict.values()))
     # write output file


Test patch:
diff --git a/src/canmatrix/tests/test_sym.py b/src/canmatrix/tests/test_sym.py
index cb99d691..ff899745 100644
--- a/src/canmatrix/tests/test_sym.py
+++ b/src/canmatrix/tests/test_sym.py
@@ -212,13 +212,49 @@ def test_enums_read(enum_str, enum_dict, enum_label):
 {{ENUMS}}
 {}
 '''.format(enum_str).encode('utf-8'),
-        )
+    )
 
         matrix = canmatrix.formats.sym.load(f)
         assert matrix.load_errors == [], "Failed to load canmatrix, when testing enum case : '{}'".format(enum_label)
         assert matrix.value_tables == enum_dict, "Enum not parsed correctly : '{}'".format(enum_label)
-        f_out = io.BytesIO()
-        canmatrix.formats.sym.dump(matrix, f_out)
+
+
+def test_enums_export():
+    f = io.BytesIO('''\
+FormatVersion=5.0 // Do not edit this line!
+Title="An Example Title"
+
+{ENUMS}
+enum Animal(0="Dog",1="Cat",2="Fox")
+
+{SENDRECEIVE}
+
+[Frame1]
+ID=000h
+DLC=8
+Var=Signal1 unsigned 0,16
+'''.encode('utf-8'),
+    )
+
+    matrix = canmatrix.formats.sym.load(f)
+    assert matrix.load_errors == [], "Failed to load canmatrix"
+
+    # Add an enum to Signal1
+    matrix.frame_by_name("Frame1").signal_by_name("Signal1").enumeration = "Plants"
+    matrix.frame_by_name("Frame1").signal_by_name("Signal1").values = {0: "Grass", 1: "Flower", 2: "Tree"}
+
+    # Export and reimport
+    f_out = io.BytesIO()
+    canmatrix.formats.sym.dump(matrix, f_out)
+    f_in = io.BytesIO(f_out.getvalue())
+    new_matrix = canmatrix.formats.sym.load(f_in)
+
+    # Check that Enums from Enums table exported and reimported correctly
+    assert new_matrix.value_tables["Animal"] == {0: "Dog", 1: "Cat", 2: "Fox"}
+
+    # Check that Enums from a Signal.Values property exported and reimported correctly
+    assert new_matrix.value_tables["Plants"] == {0: "Grass", 1: "Flower", 2: "Tree"}
+
 
 
 def test_types_read():