Repository: DHI/mikeio

Issue:
PFS. read write of parameter list, which contains both boolean and string
MIKE IO version 1.2.dev0

This is probably a rare case, but it seems that this line will fail: 

    fill_list = false, 'TEST'

if you read it from a pfs file and try to write it again you will get this: 

    fill_list = 'false, 'TEST''

Which is different. This only occurs if the line contains both boolean and string.  

Here is another example: 

    fill_list = 0,'TEST',false

which gives

    fill_list = '0,'TEST',false'

Some other combined cases can be even worse(!) - the below line cannot be parsed: 

     fill_list = 'dsd', 0, 0.0, false

Gives this error:

      E           expected <block end>, but found ','
      E             in "<unicode string>", line 16, column 22:
      E                   fill_list:  'dsd', 0.0, false

Probably because the string is first on the line. 

Comments:

Patch:
diff --git a/mikeio/pfs.py b/mikeio/pfs.py
index 10d80fce..e8812520 100644
--- a/mikeio/pfs.py
+++ b/mikeio/pfs.py
@@ -203,8 +203,11 @@ def parse_yaml_preserving_duplicates(src, unique_keywords=True):
                         data[key] = []
                     data[key].append(val)
                 else:
-                    warnings.warn(f"Keyword {key} defined multiple times. Value: {val}")
-                    data[key] = val
+                    warnings.warn(
+                        f"Keyword {key} defined multiple times (first will be used). Value: {val}"
+                    )
+                    if key not in data:
+                        data[key] = val
             else:
                 data[key] = val
         return data
@@ -243,7 +246,7 @@ class Pfs:
     """
 
     def __init__(self, input, encoding="cp1252", names=None, unique_keywords=True):
-        if isinstance(input, (str, Path)):
+        if isinstance(input, (str, Path)) or hasattr(input, "read"):
             if names is not None:
                 raise ValueError("names cannot be given as argument if input is a file")
             sections, names = self._read_pfs_file(input, encoding, unique_keywords)
@@ -404,8 +407,11 @@ class Pfs:
 
     def _pfs2yaml(self, filename, encoding=None) -> str:
 
-        with (open(filename, encoding=encoding)) as f:
-            pfsstring = f.read()
+        if hasattr(filename, "read"):  # To read in memory strings StringIO
+            pfsstring = filename.read()
+        else:
+            with (open(filename, encoding=encoding)) as f:
+                pfsstring = f.read()
 
         lines = pfsstring.split("\n")
 
@@ -453,7 +459,9 @@ class Pfs:
                 key = key.strip()
                 value = s[(idx + 1) :]
 
-                if s.count("'") == 2:  # This is a quoted string and not a list
+                if (
+                    s[0] == "'" and s[-1] == "'"
+                ):  # This is a quoted string and not a list
                     s = s
                 else:
                     if "," in value:


Test patch:
diff --git a/tests/test_pfs.py b/tests/test_pfs.py
index 1bf1e42e..6355799d 100644
--- a/tests/test_pfs.py
+++ b/tests/test_pfs.py
@@ -1,3 +1,4 @@
+from io import StringIO
 import sys
 import os
 import pytest
@@ -375,8 +376,8 @@ def test_non_unique_keywords():
     assert len(pfs.BoundaryExtractor.POINT_1) == 2
     assert isinstance(pfs.BoundaryExtractor.POINT_1[1], mikeio.PfsSection)
 
-    # last value will be kept
-    assert pfs.BoundaryExtractor.z_min == 19
+    # first value will be kept (like MIKE FM)
+    assert pfs.BoundaryExtractor.z_min == -3000
 
 
 def test_non_unique_keywords_allowed():
@@ -419,3 +420,97 @@ def test_mdf():
         pfs.data[2].POLYGONS.Data
         == "8 8 Untitled 359236.79224376212 6168403.076453222 1 0 -1 -1 16711680 65535 3 0 1 0 1 1000 1000 0 0 0 0 0 1 1000 2 2 0 10 3 38 32 25 8 Untitled 367530.58488032949 6174892.7846391136 0 0 -1 -1 16711680 65535 3 1 700000 0 1 1000 1000 0 0 0 0 0 1 1000 2 2 0 10 14 34 25 32 39 1 37 35 31 23 26 17 30 22 24 8 Untitled 358191.86702583247 6164004.5695307152 1 0 -1 -1 16711680 65535 3 0 1 0 1 1000 1000 0 0 0 0 0 1 1000 2 2 0 10 2 1 36 8 Untitled 356300.2080261847 6198016.2887355704 1 0 -1 -1 16711680 65535 3 0 1 0 1 1000 1000 0 0 0 0 0 1 1000 2 2 0 10 2 9 0 9 Ndr Roese 355957.23455536627 6165986.6140259188 0 0 -1 -1 16711680 65535 3 1 180000 0 1 1000 1000 0 0 0 0 0 1 1000 2 2 0 10 6 33 37 36 39 38 34 16 Area of interest 355794.66401566722 6167799.1149176853 0 0 -1 -1 16711680 65535 3 1 50000 0 1 1000 1000 0 0 0 0 0 1 1000 2 2 0 10 1 40 8 Untitled 353529.91916129418 6214840.5979535272 0 0 -1 -1 16711680 65535 3 1 700000 0 1 1000 1000 0 0 0 0 0 1 1000 2 2 0 10 8 41 8 7 27 4 6 11 12 8 Untitled 351165.00127937191 6173083.0605236143 1 0 -1 -1 16711680 65535 3 0 1 0 1 1000 1000 0 0 0 0 0 1 1000 2 2 0 10 1 2 "
     )
+
+
+def test_read_in_memory_string():
+
+    text = """
+[ENGINE]
+  option = foo,bar
+EndSect // ENGINE
+"""
+    pfs = mikeio.Pfs(StringIO(text))
+
+    assert pfs.ENGINE.option == ["foo", "bar"]
+
+
+def test_read_mixed_array():
+
+    text = """
+[ENGINE]
+  advanced= false
+  fill_list = false, 'TEST'
+EndSect // ENGINE
+"""
+    pfs = mikeio.Pfs(StringIO(text))
+
+    assert pfs.ENGINE.advanced == False
+    assert isinstance(pfs.ENGINE.fill_list, (list, tuple))
+    assert len(pfs.ENGINE.fill_list) == 2
+    assert pfs.ENGINE.fill_list[0] == False
+    assert pfs.ENGINE.fill_list[1] == "TEST"
+
+
+def test_read_mixed_array2():
+
+    text = """
+[ENGINE]
+  fill_list = 'dsd', 0, 0.0, false
+EndSect // ENGINE
+"""
+    pfs = mikeio.Pfs(StringIO(text))
+    assert isinstance(pfs.ENGINE.fill_list, (list, tuple))
+    assert len(pfs.ENGINE.fill_list) == 4
+    assert pfs.ENGINE.fill_list[0] == "dsd"
+    assert pfs.ENGINE.fill_list[1] == 0
+    assert pfs.ENGINE.fill_list[2] == 0.0
+    assert pfs.ENGINE.fill_list[3] == False
+
+
+def test_read_mixed_array3():
+
+    text = """
+[ENGINE]
+  fill_list = 'dsd', 0, 0.0, "str2", false, 'str3'
+EndSect // ENGINE
+"""
+    pfs = mikeio.Pfs(StringIO(text))
+    assert isinstance(pfs.ENGINE.fill_list, (list, tuple))
+    assert len(pfs.ENGINE.fill_list) == 6
+    assert pfs.ENGINE.fill_list[0] == "dsd"
+    assert pfs.ENGINE.fill_list[1] == 0
+    assert pfs.ENGINE.fill_list[2] == 0.0
+    assert pfs.ENGINE.fill_list[3] == "str2"
+    assert pfs.ENGINE.fill_list[4] == False
+    assert pfs.ENGINE.fill_list[5] == "str3"
+
+
+def test_read_array():
+
+    text = """
+[ENGINE]
+  fill_list = 1, 2
+EndSect // ENGINE
+"""
+    pfs = mikeio.Pfs(StringIO(text))
+
+    assert isinstance(pfs.ENGINE.fill_list, (list, tuple))
+    assert len(pfs.ENGINE.fill_list) == 2
+    assert pfs.ENGINE.fill_list[0] == 1
+    assert pfs.ENGINE.fill_list[1] == 2
+
+
+def test_read_string_array():
+
+    text = """
+[ENGINE]
+  fill_list = 'foo', 'bar', 'baz'
+EndSect // ENGINE
+"""
+    pfs = mikeio.Pfs(StringIO(text))
+
+    assert isinstance(pfs.ENGINE.fill_list, (list, tuple))
+    assert len(pfs.ENGINE.fill_list) == 3
+    assert pfs.ENGINE.fill_list[0] == "foo"
+    assert pfs.ENGINE.fill_list[1] == "bar"
+    assert pfs.ENGINE.fill_list[2] == "baz"