From 5e983afdcb145da25e7da334cbb3dbf7ec2d940f Mon Sep 17 00:00:00 2001
From: Karl-Hermann Wieners <karl-hermann.wieners@mpimet.mpg.de>
Date: Mon, 28 Nov 2022 14:26:06 +0100
Subject: [PATCH] Config: add shared namelist settings ([[namelist_a,
 namelist_b]])

---
 CHANGES.txt  |  5 +++
 expconfig.py | 46 +++++++++++++++++----------
 test.py      | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 122 insertions(+), 17 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 698ff2c..b40c3c4 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -9,6 +9,11 @@ Release Changes
 Release 1.2.1
 =============
 
+Config
+------
+
+* Added shared namelist settings ([[namelist_a, namelist_b]])
+
 Release 1.2.0
 =============
 
diff --git a/expconfig.py b/expconfig.py
index d3a833a..6c95dbb 100644
--- a/expconfig.py
+++ b/expconfig.py
@@ -137,18 +137,32 @@ class ExpConfig(ConfigObj):
         # Helper functions
         #
 
-        def split_jobs(config):
-            '''Post-process job definition to allow for shared configs as [[job1, job2]]'''
-            if 'jobs' in config:
-                sep = re.compile(r'\s*,\s*')
-                for subjobs, subconfig in config['jobs'].items():
-                    if re.search(sep, subjobs):
-                        for subjob in re.split(sep, subjobs):
-                            if subjob in config['jobs']:
-                                config['jobs'][subjob].merge(subconfig.dict())
+        def split_shared_sections(config):
+            '''Process sections to expand shared entries as [[job1, job2]]
+
+               Supports jobs, namelists, and job-specific namelists'''
+
+            sep = re.compile(r'\s*,\s*')
+
+            def split_key(current):
+                for subkey_orig in current.sections:
+                    subkeys = re.split(sep, subkey_orig)
+                    if len(subkeys) > 1:
+                        subconfig = current[subkey_orig]
+                        for subkey in subkeys:
+                            if subkey in current:
+                                current[subkey].merge(subconfig.dict())
                             else:
-                                config['jobs'][subjob] = subconfig.dict()
-                        del config['jobs'][subjobs]
+                                current[subkey] = subconfig.dict()
+                        del current[subkey_orig]
+
+            if 'namelists' in config.sections:
+                split_key(config['namelists'])
+            if 'jobs' in config.sections:
+                split_key(config['jobs'])
+                for job in config['jobs'].sections:
+                    if 'namelists' in config['jobs'][job].sections:
+                        split_key(config['jobs'][job]['namelists'])
 
         def get_config_name(lib_name, base_name):
             '''Cycle through config path until a match is found.
@@ -479,12 +493,12 @@ class ExpConfig(ConfigObj):
         lib_config_name = get_config_name(ExpConfig.exp_lib_dir,
                                           ExpConfig.default_name+'.config')
         pre_config.merge(ConfigObj(lib_config_name, interpolation=False))
-        split_jobs(pre_config)
+        split_shared_sections(pre_config)
         register_version(pre_config, config_versions)
 
         if os.path.exists(setup_config_name):
             pre_config.merge(ConfigObj(setup_config_name, interpolation=False))
-            split_jobs(pre_config)
+            split_shared_sections(pre_config)
             list_assign(pre_config)
             register_version(pre_config, config_versions)
 
@@ -492,7 +506,7 @@ class ExpConfig(ConfigObj):
                                           experiment_type+'.config')
         if os.path.exists(lib_config_name):
             pre_config.merge(ConfigObj(lib_config_name, interpolation=False))
-            split_jobs(pre_config)
+            split_shared_sections(pre_config)
             list_assign(pre_config)
             register_version(pre_config, config_versions)
         else:
@@ -504,7 +518,7 @@ class ExpConfig(ConfigObj):
                                               option+'.config')
             if os.path.exists(lib_config_name):
                 pre_config.merge(ConfigObj(lib_config_name, interpolation=False))
-                split_jobs(pre_config)
+                split_shared_sections(pre_config)
                 list_assign(pre_config)
                 register_version(pre_config, config_versions)
             else:
@@ -530,7 +544,7 @@ class ExpConfig(ConfigObj):
         experiment_config = ConfigObj(experiment_config_name,
                                       interpolation=False)
         pre_config.merge(experiment_config)
-        split_jobs(pre_config)
+        split_shared_sections(pre_config)
         list_assign(pre_config)
 
         # Add extra dictionary
diff --git a/test.py b/test.py
index 88a469d..f490fc1 100644
--- a/test.py
+++ b/test.py
@@ -82,12 +82,13 @@ class MkexpSimpleTestCase(MkexpTestCase):
         self.job_id = 'job'
         MkexpTestCase.setUp(self)
 
-    def run_test(self, template, expected, additional=''):
+    def run_test(self, template, expected, additional='', epilog=''):
         writeconfig(self.exp_id, u"""
             EXP_TYPE =
             """+additional+u"""
             [jobs]
               [["""+self.job_id+u"""]]
+            """+epilog+u"""
         """)
         writetemplate(self.exp_id, self.job_id, template)
         expected = align(expected)
@@ -810,6 +811,91 @@ class NamelistInheritanceTestCase(MkexpSimpleTestCase):
                 [[[group clone]]]
         """)
 
+class KeySplittingTestCase(MkexpSimpleTestCase):
+
+    def test_job_splitting(self):
+        self.run_test(u"""
+            %{{jobs.{job_id}.seniority}}
+            %{{jobs.{job_id}.common}}
+            %{{jobs.job2.seniority}}
+            %{{jobs.job2.common}}
+        """.format(**self.__dict__), u"""
+            elder
+            parents
+            younger
+            parents
+        """, epilog="""
+              seniority = elder
+            [[{job_id}, job2]]
+              common = parents
+            [[job2]]
+              .extends = {job_id}
+              seniority = younger
+        """.format(**self.__dict__))
+
+    def test_namelist_splitting(self):
+        self.run_test(u"""
+            %{NAMELIST_ATM}
+            %{NAMELIST_OCE}
+        """, u"""
+            &group1
+                value = 21
+            /
+            &group2
+                value = 42
+            /
+            &group3
+                value = 84
+            /
+            &group2
+                value = 42
+            /
+        """, u"""
+            [namelists]
+              [[NAMELIST_atm]]
+                [[[group1]]]
+                  value = 21
+              [[NAMELIST_atm, NAMELIST_oce]]
+                [[[group2]]]
+                  value = 42
+              [[NAMELIST_oce]]
+                [[[group3]]]
+                  value = 84
+        """)
+
+    def test_job_namelist_splitting(self):
+        self.run_test(u"""
+            %{NAMELIST_ATM}
+            %{NAMELIST_OCE}
+        """, u"""
+            &group1
+                value = 21
+            /
+            &group2
+                value = 42
+            /
+            &group3
+                value = 84
+            /
+            &group2
+                value = 42
+            /
+        """, u"""
+            [namelists]
+              [[NAMELIST_atm]]
+                [[[group1]]]
+                  value = 21
+              [[NAMELIST_oce]]
+                [[[group3]]]
+                  value = 84
+        """, u"""
+            [[[namelists]]]
+              [[[[NAMELIST_atm, NAMELIST_oce]]]]
+                [[[[[group2]]]]]
+                  value = 42
+        """)
+
+
 class JinjaTemplateTestCase(MkexpSimpleTestCase):
 
     def test_ignore_blocks(self):
-- 
GitLab