Repository: ESMCI/cime

Issue:
maxOccurs set to 1 for MAX_TASKS_PER_NODE breaks E3SM machine
PR #3989 introduced a change that breaks some E3SM machines.

Commit introducing the change https://github.com/sjsprecious/cime/commit/89a4292be3c78244e5042a8ad36453ae5407a7d8.

Machines broken with change
- **summit** https://github.com/E3SM-Project/E3SM/blob/d28e8771e4d33ea5e7179eeccf2e031a9838ac8c/cime_config/machines/config_machines.xml#L2715-L2742
- **ascent** https://github.com/E3SM-Project/E3SM/blob/d28e8771e4d33ea5e7179eeccf2e031a9838ac8c/cime_config/machines/config_machines.xml#L2867-L2894

Original error:
```bash
ERROR: Command: '/usr/bin/xmllint --xinclude --noout --schema E3SM/cime/config/xml_schemas/config_machines.xsd E3SM/cime_config/machines/config_machines.xml' failed with error 'E3SM/cime_config/machines/config_machines.xml:2737: element MAX_TASKS_PER_NODE: Schemas validity error : Element 'MAX_TASKS_PER_NODE': This element is not expected. Expected is one of ( MAX_GPUS_PER_NODE, MAX_MPITASKS_PER_NODE ).
E3SM/cime_config/machines/config_machines.xml:2896: element MAX_TASKS_PER_NODE: Schemas validity error : Element 'MAX_TASKS_PER_NODE': This element is not expected. Expected is one of ( MAX_GPUS_PER_NODE, MAX_MPITASKS_PER_NODE ).
E3SM/cime_config/machines/config_machines.xml fails to validate' from dir 'E3SM/cime/scripts'
```

Comments:

Patch:
diff --git a/config/xml_schemas/config_machines.xsd b/config/xml_schemas/config_machines.xsd
index 1d8dba900..3dd64f15e 100644
--- a/config/xml_schemas/config_machines.xsd
+++ b/config/xml_schemas/config_machines.xsd
@@ -159,7 +159,7 @@
         <xs:element ref="SUPPORTED_BY" minOccurs="1" maxOccurs="1"/>
         <!-- MAX_TASKS_PER_NODE: maximum number of threads*tasks per
              shared memory node on this machine-->
-        <xs:element ref="MAX_TASKS_PER_NODE" minOccurs="1" maxOccurs="1"/>
+        <xs:element ref="MAX_TASKS_PER_NODE" minOccurs="1" maxOccurs="4"/>
         <!-- MAX_GPUS_PER_NODE: maximum number of GPUs per node on this machine-->
         <xs:element ref="MAX_GPUS_PER_NODE" minOccurs="0" maxOccurs="1"/>
         <!-- MAX_MPITASKS_PER_NODE: number of physical PES per shared node on
diff --git a/scripts/lib/CIME/build.py b/scripts/lib/CIME/build.py
index 5ddeb50fd..d1b78e9e4 100644
--- a/scripts/lib/CIME/build.py
+++ b/scripts/lib/CIME/build.py
@@ -1,9 +1,12 @@
 """
 functions for building CIME models
 """
-import glob, shutil, time, threading, subprocess, imp
+import glob, shutil, time, threading, subprocess
 from CIME.XML.standard_module_setup  import *
-from CIME.utils                 import get_model, analyze_build_log, stringify_bool, run_and_log_case_status, get_timestamp, run_sub_or_cmd, run_cmd, get_batch_script_for_job, gzip_existing_file, safe_copy, check_for_python, get_logging_options
+from CIME.utils                 import get_model, analyze_build_log, \
+    stringify_bool, run_and_log_case_status, get_timestamp, run_sub_or_cmd, \
+    run_cmd, get_batch_script_for_job, gzip_existing_file, safe_copy, \
+    check_for_python, get_logging_options, import_from_file
 from CIME.provenance            import save_build_provenance as save_build_provenance_sub
 from CIME.locked_files          import lock_file, unlock_file
 from CIME.XML.files             import Files
@@ -543,7 +546,8 @@ def _create_build_metadata_for_component(config_dir, libroot, bldroot, case):
     """
     bc_path = os.path.join(config_dir, "buildlib_cmake")
     expect(os.path.exists(bc_path), "Missing: {}".format(bc_path))
-    buildlib = imp.load_source("buildlib_cmake", os.path.join(config_dir, "buildlib_cmake"))
+    buildlib = import_from_file("buildlib_cmake", os.path.join(config_dir,
+                                                               "buildlib_cmake"))
     cmake_args = buildlib.buildlib(bldroot, libroot, case)
     return "" if cmake_args is None else cmake_args
 
diff --git a/scripts/lib/CIME/utils.py b/scripts/lib/CIME/utils.py
index 10b98228d..e4ef4dd97 100644
--- a/scripts/lib/CIME/utils.py
+++ b/scripts/lib/CIME/utils.py
@@ -2,7 +2,9 @@
 Common functions used by cime python scripts
 Warning: you cannot use CIME Classes in this module as it causes circular dependencies
 """
-import io, logging, gzip, sys, os, time, re, shutil, glob, string, random, imp, fnmatch
+import io, logging, gzip, sys, os, time, re, shutil, glob, string, random, \
+    importlib, fnmatch
+import importlib.util
 import errno, signal, warnings, filecmp
 import stat as statlib
 import six
@@ -15,6 +17,19 @@ from distutils import file_util
 TESTS_FAILED_ERR_CODE = 100
 logger = logging.getLogger(__name__)
 
+def import_from_file(name, file_path):
+    loader = importlib.machinery.SourceFileLoader(name, file_path)
+
+    spec = importlib.util.spec_from_loader(loader.name, loader)
+
+    module = importlib.util.module_from_spec(spec)
+
+    sys.modules[name] = module
+
+    spec.loader.exec_module(module)
+
+    return module
+
 @contextmanager
 def redirect_stdout(new_target):
     old_target, sys.stdout = sys.stdout, new_target # replace sys.stdout
@@ -414,7 +429,7 @@ def run_sub_or_cmd(cmd, cmdargs, subname, subargs, logfile=None, case=None,
 
     if not do_run_cmd:
         try:
-            mod = imp.load_source(subname, cmd)
+            mod = import_from_file(subname, cmd)
             logger.info("   Calling {}".format(cmd))
             # Careful: logfile code is not thread safe!
             if logfile:


Test patch:
diff --git a/scripts/lib/CIME/tests/test_utils.py b/scripts/lib/CIME/tests/test_utils.py
index dde52eff8..2759b5352 100644
--- a/scripts/lib/CIME/tests/test_utils.py
+++ b/scripts/lib/CIME/tests/test_utils.py
@@ -2,10 +2,12 @@
 
 import os
 import sys
+import tempfile
 
 import unittest
 from unittest import mock
-from CIME.utils import indent_string, run_and_log_case_status
+from CIME.utils import indent_string, run_and_log_case_status, \
+    import_from_file
 
 from . import utils
 
@@ -87,6 +89,19 @@ class TestUtils(unittest.TestCase):
 
         self.assertTrue(result, msg="\n".join(error))
 
+    def test_import_from_file(self):
+        with tempfile.NamedTemporaryFile() as fd:
+            fd.writelines([
+                b"def test():\n",
+                b"  return 'value'",
+            ])
+
+            fd.flush()
+
+            module = import_from_file("test.py", fd.name)
+
+            assert module.test() == "value"
+
     def test_run_and_log_case_status(self):
         test_lines = [
             "00:00:00 default starting \n",
diff --git a/scripts/tests/scripts_regression_tests.py b/scripts/tests/scripts_regression_tests.py
index 5c2377a97..6279fbbb1 100755
--- a/scripts/tests/scripts_regression_tests.py
+++ b/scripts/tests/scripts_regression_tests.py
@@ -22,7 +22,9 @@ import stat as osstat
 
 import collections
 
-from CIME.utils import run_cmd, run_cmd_no_fail, get_lids, get_current_commit, safe_copy, CIMEError, get_cime_root, get_src_root, Timeout
+from CIME.utils import run_cmd, run_cmd_no_fail, get_lids, get_current_commit, \
+    safe_copy, CIMEError, get_cime_root, get_src_root, Timeout, \
+    import_from_file
 import get_tests
 import CIME.test_scheduler, CIME.wait_for_tests
 from  CIME.test_scheduler import TestScheduler
@@ -2431,19 +2433,16 @@ class K_TestCimeCase(TestCreateTestCommon):
     ###########################################################################
     def test_case_submit_interface(self):
     ###########################################################################
-        try:
-            import imp
-        except ImportError:
-            print("imp not found, skipping case.submit interface test")
-            return
         # the current directory may not exist, so make sure we are in a real directory
         os.chdir(os.getenv("HOME"))
         sys.path.append(TOOLS_DIR)
         case_submit_path = os.path.join(TOOLS_DIR, "case.submit")
-        submit_interface = imp.load_source("case_submit_interface", case_submit_path)
+
+        module = import_from_file("case.submit", case_submit_path)
+
         sys.argv = ["case.submit", "--batch-args", "'random_arguments_here.%j'",
                     "--mail-type", "fail", "--mail-user", "'random_arguments_here.%j'"]
-        submit_interface._main_func(None, True)
+        module._main_func(None, True)
 
     ###########################################################################
     def test_xml_caching(self):