Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision bb4b50990533a15ceda4443988db938e13a28a2b)
+++ sasmodels/modelinfo.py	(revision 724257c92ca5f608f1dc18211ee76a0f7d2fe6f1)
@@ -722,5 +722,5 @@
     parameters = make_parameter_table(getattr(kernel_module, 'parameters', []))
     demo = expand_pars(parameters, getattr(kernel_module, 'demo', None))
-    filename = abspath(kernel_module.__file__)
+    filename = abspath(kernel_module.__file__).replace('.pyc', '.py')
     kernel_id = splitext(basename(filename))[0]
     name = getattr(kernel_module, 'name', None)
@@ -729,5 +729,5 @@
 
     info.id = kernel_id  # string used to load the kernel
-    info.filename = abspath(kernel_module.__file__)
+    info.filename = filename
     info.name = name
     info.title = getattr(kernel_module, 'title', name+" model")
Index: sasmodels/sasview_model.py
===================================================================
--- sasmodels/sasview_model.py	(revision 749a7d468b6283673aed0714004d4af77da3f2c9)
+++ sasmodels/sasview_model.py	(revision 724257c92ca5f608f1dc18211ee76a0f7d2fe6f1)
@@ -15,5 +15,5 @@
 import traceback
 import logging
-from os.path import basename, splitext
+from os.path import basename, splitext, abspath, getmtime
 import thread
 
@@ -40,7 +40,119 @@
     pass
 
+logger = logging.getLogger(__name__)
+
 calculation_lock = thread.allocate_lock()
 
+#: True if pre-existing plugins, with the old names and parameters, should
+#: continue to be supported.
 SUPPORT_OLD_STYLE_PLUGINS = True
+
+# TODO: separate x_axis_label from multiplicity info
+MultiplicityInfo = collections.namedtuple(
+    'MultiplicityInfo',
+    ["number", "control", "choices", "x_axis_label"],
+)
+
+#: set of defined models (standard and custom)
+MODELS = {}  # type: Dict[str, SasviewModelType]
+#: custom model {path: model} mapping so we can check timestamps
+MODEL_BY_PATH = {}  # type: Dict[str, SasviewModelType]
+
+def find_model(modelname):
+    # type: (str) -> SasviewModelType
+    """
+    Find a model by name.  If the model name ends in py, try loading it from
+    custom models, otherwise look for it in the list of builtin models.
+    """
+    # TODO: used by sum/product model to load an existing model
+    # TODO: doesn't handle custom models properly
+    if modelname.endswith('.py'):
+        return load_custom_model(modelname)
+    elif modelname in MODELS:
+        return MODELS[modelname]
+    else:
+        raise ValueError("unknown model %r"%modelname)
+
+
+# TODO: figure out how to say that the return type is a subclass
+def load_standard_models():
+    # type: () -> List[SasviewModelType]
+    """
+    Load and return the list of predefined models.
+
+    If there is an error loading a model, then a traceback is logged and the
+    model is not returned.
+    """
+    for name in core.list_models():
+        try:
+            MODELS[name] = _make_standard_model(name)
+        except Exception:
+            logger.error(traceback.format_exc())
+    if SUPPORT_OLD_STYLE_PLUGINS:
+        _register_old_models()
+
+    return list(MODELS.values())
+
+
+def load_custom_model(path):
+    # type: (str) -> SasviewModelType
+    """
+    Load a custom model given the model path.
+    """
+    model = MODEL_BY_PATH.get(path, None)
+    if model is not None and model.timestamp == getmtime(path):
+        #logger.info("Model already loaded %s", path)
+        return model
+
+    #logger.info("Loading model %s", path)
+    kernel_module = custom.load_custom_kernel_module(path)
+    if hasattr(kernel_module, 'Model'):
+        model = kernel_module.Model
+        # Old style models do not set the name in the class attributes, so
+        # set it here; this name will be overridden when the object is created
+        # with an instance variable that has the same value.
+        if model.name == "":
+            model.name = splitext(basename(path))[0]
+        if not hasattr(model, 'filename'):
+            model.filename = abspath(kernel_module.__file__).replace('.pyc', '.py')
+        if not hasattr(model, 'id'):
+            model.id = splitext(basename(model.filename))[0]
+    else:
+        model_info = modelinfo.make_model_info(kernel_module)
+        model = _make_model_from_info(model_info)
+    model.timestamp = getmtime(path)
+
+    # If a model name already exists and we are loading a different model,
+    # use the model file name as the model name.
+    if model.name in MODELS and not model.filename == MODELS[model.name].filename:
+        _previous_name = model.name
+        model.name = model.id
+
+        # If the new model name is still in the model list (for instance,
+        # if we put a cylinder.py in our plug-in directory), then append
+        # an identifier.
+        if model.name in MODELS and not model.filename == MODELS[model.name].filename:
+            model.name = model.id + '_user'
+        logger.info("Model %s already exists: using %s [%s]",
+                    _previous_name, model.name, model.filename)
+
+    MODELS[model.name] = model
+    MODEL_BY_PATH[path] = model
+    return model
+
+
+def _make_standard_model(name):
+    # type: (str) -> SasviewModelType
+    """
+    Load the sasview model defined by *name*.
+
+    *name* can be a standard model name or a path to a custom model.
+
+    Returns a class that can be used directly as a sasview model.
+    """
+    kernel_module = generate.load_kernel_module(name)
+    model_info = modelinfo.make_model_info(kernel_module)
+    return _make_model_from_info(model_info)
+
 
 def _register_old_models():
@@ -60,5 +172,5 @@
     import sas.models
     from sasmodels.conversion_table import CONVERSION_TABLE
-    for new_name, conversion in CONVERSION_TABLE.get((3,1,2), {}).items():
+    for new_name, conversion in CONVERSION_TABLE.get((3, 1, 2), {}).items():
         # CoreShellEllipsoidModel => core_shell_ellipsoid:1
         new_name = new_name.split(':')[0]
@@ -69,107 +181,4 @@
         setattr(sas.models, old_path, ConstructedModule)
         sys.modules[old_path] = ConstructedModule
-
-
-# TODO: separate x_axis_label from multiplicity info
-MultiplicityInfo = collections.namedtuple(
-    'MultiplicityInfo',
-    ["number", "control", "choices", "x_axis_label"],
-)
-
-MODELS = {}
-def find_model(modelname):
-    # type: (str) -> SasviewModelType
-    """
-    Find a model by name.  If the model name ends in py, try loading it from
-    custom models, otherwise look for it in the list of builtin models.
-    """
-    # TODO: used by sum/product model to load an existing model
-    # TODO: doesn't handle custom models properly
-    if modelname.endswith('.py'):
-        return load_custom_model(modelname)
-    elif modelname in MODELS:
-        return MODELS[modelname]
-    else:
-        raise ValueError("unknown model %r"%modelname)
-
-
-# TODO: figure out how to say that the return type is a subclass
-def load_standard_models():
-    # type: () -> List[SasviewModelType]
-    """
-    Load and return the list of predefined models.
-
-    If there is an error loading a model, then a traceback is logged and the
-    model is not returned.
-    """
-    models = []
-    for name in core.list_models():
-        try:
-            MODELS[name] = _make_standard_model(name)
-            models.append(MODELS[name])
-        except Exception:
-            logging.error(traceback.format_exc())
-    if SUPPORT_OLD_STYLE_PLUGINS:
-        _register_old_models()
-
-    return models
-
-
-def load_custom_model(path):
-    # type: (str) -> SasviewModelType
-    """
-    Load a custom model given the model path.
-    """
-    kernel_module = custom.load_custom_kernel_module(path)
-    try:
-        model = kernel_module.Model
-        # Old style models do not set the name in the class attributes, so
-        # set it here; this name will be overridden when the object is created
-        # with an instance variable that has the same value.
-        if model.name == "":
-            model.name = splitext(basename(path))[0]
-        if not hasattr(model, 'filename'):
-            model.filename = kernel_module.__file__
-            # For old models, treat .pyc and .py files interchangeably.
-            # This is needed because of the Sum|Multi(p1,p2) types of models
-            # and the convoluted way in which they are created.
-            if model.filename.endswith(".py"):
-                logging.info("Loading %s as .pyc", model.filename)
-                model.filename = model.filename+'c'
-        if not hasattr(model, 'id'):
-            model.id = splitext(basename(model.filename))[0]
-    except AttributeError:
-        model_info = modelinfo.make_model_info(kernel_module)
-        model = _make_model_from_info(model_info)
-
-    # If a model name already exists and we are loading a different model,
-    # use the model file name as the model name.
-    if model.name in MODELS and not model.filename == MODELS[model.name].filename:
-        _previous_name = model.name
-        model.name = model.id
-        
-        # If the new model name is still in the model list (for instance,
-        # if we put a cylinder.py in our plug-in directory), then append
-        # an identifier.
-        if model.name in MODELS and not model.filename == MODELS[model.name].filename:
-            model.name = model.id + '_user'
-        logging.info("Model %s already exists: using %s [%s]", _previous_name, model.name, model.filename)
-
-    MODELS[model.name] = model
-    return model
-
-
-def _make_standard_model(name):
-    # type: (str) -> SasviewModelType
-    """
-    Load the sasview model defined by *name*.
-
-    *name* can be a standard model name or a path to a custom model.
-
-    Returns a class that can be used directly as a sasview model.
-    """
-    kernel_module = generate.load_kernel_module(name)
-    model_info = modelinfo.make_model_info(kernel_module)
-    return _make_model_from_info(model_info)
 
 
@@ -283,9 +292,9 @@
 
     #: names of the orientation parameters in the order they appear
-    orientation_params = None # type: Sequence[str]
+    orientation_params = None # type: List[str]
     #: names of the magnetic parameters in the order they appear
-    magnetic_params = None    # type: Sequence[str]
+    magnetic_params = None    # type: List[str]
     #: names of the fittable parameters
-    fixed = None              # type: Sequence[str]
+    fixed = None              # type: List[str]
     # TODO: the attribute fixed is ill-named
 
@@ -611,6 +620,6 @@
         ## to calculate_Iq
         #if calculation_lock.locked():
-        #    logging.info("calculation waiting for another thread to complete")
-        #    logging.info("\n".join(traceback.format_stack()))
+        #    logger.info("calculation waiting for another thread to complete")
+        #    logger.info("\n".join(traceback.format_stack()))
 
         with calculation_lock:
