Repository: geographika/mappyfile

Issue:
layers.insert fails with dict error
maybe related to python +3.6?

as suggested at https://mappyfile.readthedocs.io/en/latest/#adding-items
```
mf = mappyfile.open("./default.map")
lyrs = mf['layers']
lyrs.insert(0,mslr)
```
throws:
```
lyrs.insert(0,mslr)
AttributeError: 'CaseInsensitiveOrderedDict' object has no attribute 'insert'
```

python 3.8.10
mappyfile 0.9.4

Discussion:

Patch:
diff --git a/mappyfile.pyproj b/mappyfile.pyproj
index 5655df3..91a95a6 100644
--- a/mappyfile.pyproj
+++ b/mappyfile.pyproj
@@ -12,7 +12,7 @@
     <Name>mappyfile</Name>
     <RootNamespace>mappyfile</RootNamespace>
     <InterpreterId>MSBuild|mappyfile|$(MSBuildProjectFullPath)</InterpreterId>
-    <StartupFile>tests\test_utils.py</StartupFile>
+    <StartupFile>tests\test_ordereddict.py</StartupFile>
     <LaunchProvider>Standard Python launcher</LaunchProvider>
     <EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
     <Environment>PATH=C:\MapServer\bin;%PATH%
@@ -1141,15 +1141,6 @@ PROJ_LIB=C:\MapServer\bin\proj\SHARE
     </Compile>
   </ItemGroup>
   <ItemGroup>
-    <Interpreter Include="C:\VirtualEnvs\mappyfile3\">
-      <Id>mappyfile3</Id>
-      <Version>3.7</Version>
-      <Description>mappyfile3 (Python 3.7 (64-bit))</Description>
-      <InterpreterPath>Scripts\python.exe</InterpreterPath>
-      <WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
-      <PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
-      <Architecture>X64</Architecture>
-    </Interpreter>
     <Interpreter Include="C:\VirtualEnvs\mappyfile\">
       <Id>mappyfile</Id>
       <Version>2.7</Version>
diff --git a/mappyfile/ordereddict.py b/mappyfile/ordereddict.py
index 3a8d4fb..851a4de 100644
--- a/mappyfile/ordereddict.py
+++ b/mappyfile/ordereddict.py
@@ -38,6 +38,7 @@
 from collections import OrderedDict
 import copy
 import sys
+from mappyfile.tokens import OBJECT_LIST_KEYS
 
 
 try:
@@ -77,7 +78,12 @@ class DefaultOrderedDict(OrderedDict):
     def __missing__(self, key):
         if self.default_factory is None:
             raise KeyError(key)
-        self[key] = value = self.default_factory()
+
+        if key in OBJECT_LIST_KEYS:
+            # create empty lists for keys of object lists such as "layers"
+            self[key] = value = []
+        else:
+            self[key] = value = self.default_factory()
         return value
 
     def __reduce__(self):
diff --git a/mappyfile/pprint.py b/mappyfile/pprint.py
index eaffcaf..355751e 100644
--- a/mappyfile/pprint.py
+++ b/mappyfile/pprint.py
@@ -31,7 +31,8 @@ from __future__ import unicode_literals
 import sys
 import logging
 import numbers
-from mappyfile.tokens import COMPOSITE_NAMES, SINGLETON_COMPOSITE_NAMES, REPEATED_KEYS, COMPLEX_TYPES
+from mappyfile.tokens import COMPOSITE_NAMES, SINGLETON_COMPOSITE_NAMES, \
+                             REPEATED_KEYS, COMPLEX_TYPES, OBJECT_LIST_KEYS
 from mappyfile.validator import Validator
 import mappyfile as utils
 
@@ -352,9 +353,7 @@ class PrettyPrinter(object):
         values are a list
         """
 
-        if key in ("layers", "classes", "styles", "symbols", "labels",
-                   "outputformats", "features", "scaletokens",
-                   "composites", "joins") and isinstance(val, list):
+        if key in OBJECT_LIST_KEYS and isinstance(val, list):
             return True
         else:
             return False
diff --git a/mappyfile/tokens.py b/mappyfile/tokens.py
index e021c91..6a85477 100644
--- a/mappyfile/tokens.py
+++ b/mappyfile/tokens.py
@@ -395,3 +395,18 @@ ms_errorfile
 
 # some keywords can be added multiple times to a composite type
 REPEATED_KEYS = ('processing', 'formatoption', 'include', 'data')
+
+# these are keywords used in the schema to store collections of composite objects
+# for example lists of lyaers
+OBJECT_LIST_KEYS = frozenset("""
+layers
+classes
+styles
+symbols
+labels
+outputformats
+features
+scaletokens
+composites
+joins
+""".split())


Test patch:
diff --git a/tests/test_ordereddict.py b/tests/test_ordereddict.py
index 301a48e..62b081f 100644
--- a/tests/test_ordereddict.py
+++ b/tests/test_ordereddict.py
@@ -5,6 +5,7 @@ import inspect
 import tempfile
 import pickle
 import pytest
+import mappyfile
 from mappyfile.parser import Parser
 from mappyfile.pprint import PrettyPrinter
 from mappyfile.transformer import MapfileToDict
@@ -184,6 +185,31 @@ def test_deepcopy():
     assert c['__type__'] == 'layer'
 
 
+def test_adding_layers():
+    """
+    Test adding layers to an existing collection
+    """
+    m = mappyfile.loads("MAP LAYER TYPE POLYGON NAME 'TEST' END END")
+    lyr = mappyfile.loads("LAYER TYPE POLYGON NAME 'TEST' END")
+
+    assert len(m["layers"]) == 1
+    m["layers"].insert(0, lyr)
+    assert len(m["layers"]) == 2
+
+
+def test_adding_layers_to_empty():
+    """
+    Test adding layers when the layers object has not yet been created
+    """
+    m = mappyfile.loads("MAP END")
+    lyr = mappyfile.loads("LAYER TYPE POLYGON NAME 'TEST' END")
+
+    assert len(m["layers"]) == 0
+    m["layers"].insert(0, lyr)
+    assert len(m["layers"]) == 1
+    print(mappyfile.dumps(m))
+
+
 def run_tests():
     # pytest.main(["tests/test_ordereddict.py::test_dict"])
     pytest.main(["tests/test_ordereddict.py"])
@@ -193,5 +219,5 @@ if __name__ == '__main__':
     logging.basicConfig(level=logging.DEBUG)
     # run_tests()
     # test_update_case_sensitive_ordered_dict()
-    test_pop()
+    test_adding_layers_to_empty()
     print("Done!")