[cd0a808] | 1 | """ |
---|
| 2 | Custom Models |
---|
| 3 | ------------- |
---|
| 4 | |
---|
| 5 | This is a place holder for the custom models namespace. When models are |
---|
| 6 | loaded from a file by :func:`generate.load_kernel_module` they are loaded |
---|
| 7 | as if they exist in *sasmodels.custom*. This package needs to exist for this |
---|
| 8 | to occur without error. |
---|
| 9 | """ |
---|
[3c852d4] | 10 | from __future__ import division, print_function |
---|
[cd0a808] | 11 | |
---|
[3c852d4] | 12 | import sys |
---|
[cd0a808] | 13 | import os |
---|
[d321747] | 14 | from os.path import basename, splitext, join as joinpath, exists, dirname |
---|
[cd0a808] | 15 | |
---|
| 16 | try: |
---|
| 17 | # Python 3.5 and up |
---|
[7ae2b7f] | 18 | from importlib.util import spec_from_file_location, module_from_spec # type: ignore |
---|
[cd0a808] | 19 | def load_module_from_path(fullname, path): |
---|
[d321747] | 20 | # type: (str, str) -> "module" |
---|
[40a87fa] | 21 | """load module from *path* as *fullname*""" |
---|
[2a0c7a6] | 22 | spec = spec_from_file_location(fullname, os.path.expanduser(path)) |
---|
[cd0a808] | 23 | module = module_from_spec(spec) |
---|
| 24 | spec.loader.exec_module(module) |
---|
| 25 | return module |
---|
| 26 | except ImportError: |
---|
| 27 | # CRUFT: python 2 |
---|
| 28 | import imp |
---|
| 29 | def load_module_from_path(fullname, path): |
---|
[d321747] | 30 | # type: (str, str) -> "module" |
---|
[40a87fa] | 31 | """load module from *path* as *fullname*""" |
---|
[3c852d4] | 32 | # Clear out old definitions, if any |
---|
| 33 | if fullname in sys.modules: |
---|
| 34 | del sys.modules[fullname] |
---|
[0f48f1e] | 35 | if path.endswith(".py") and os.path.exists(path) and os.path.exists(path+"c"): |
---|
| 36 | # remove automatic pyc file before loading a py file |
---|
| 37 | os.unlink(path+"c") |
---|
[2a0c7a6] | 38 | module = imp.load_source(fullname, os.path.expanduser(path)) |
---|
[cd0a808] | 39 | return module |
---|
| 40 | |
---|
[d321747] | 41 | _MODULE_CACHE = {} # type: Dict[str, Tuple("module", int)] |
---|
| 42 | _MODULE_DEPENDS = {} # type: Dict[str, List[str]] |
---|
| 43 | _MODULE_DEPENDS_STACK = [] # type: List[str] |
---|
[cd0a808] | 44 | def load_custom_kernel_module(path): |
---|
[d321747] | 45 | # type: str -> "module" |
---|
[40a87fa] | 46 | """load SAS kernel from *path* as *sasmodels.custom.modelname*""" |
---|
[cd0a808] | 47 | # Pull off the last .ext if it exists; there may be others |
---|
| 48 | name = basename(splitext(path)[0]) |
---|
[839fd68] | 49 | path = os.path.expanduser(path) |
---|
[91bd550] | 50 | |
---|
[d321747] | 51 | # Reload module if necessary. |
---|
[91bd550] | 52 | if need_reload(path): |
---|
[d321747] | 53 | # Assume the module file is the only dependency |
---|
[91bd550] | 54 | _MODULE_DEPENDS[path] = set([path]) |
---|
| 55 | |
---|
[d321747] | 56 | # Load the module while pushing it onto the dependency stack. If |
---|
| 57 | # this triggers any submodules, then they will add their dependencies |
---|
| 58 | # to this module as the "working_on" parent. Pop the stack when the |
---|
| 59 | # module is loaded. |
---|
| 60 | _MODULE_DEPENDS_STACK.append(path) |
---|
[839fd68] | 61 | module = load_module_from_path('sasmodels.custom.'+name, path) |
---|
[91bd550] | 62 | _MODULE_DEPENDS_STACK.pop() |
---|
| 63 | |
---|
[d321747] | 64 | # Include external C code in the dependencies. We are looking |
---|
| 65 | # for module.source and assuming that it is a list of C source files |
---|
| 66 | # relative to the module itself. Any files that do not exist, |
---|
| 67 | # such as those in the standard libraries, will be ignored. |
---|
| 68 | # TODO: look in builtin module path for standard c sources |
---|
| 69 | # TODO: share code with generate.model_sources |
---|
| 70 | c_sources = getattr(module, 'source', None) |
---|
| 71 | if isinstance(c_sources, (list, tuple)): |
---|
| 72 | _MODULE_DEPENDS[path].update(_find_sources(path, c_sources)) |
---|
[91bd550] | 73 | |
---|
[d321747] | 74 | # Cache the module, and tag it with the newest timestamp |
---|
[91bd550] | 75 | timestamp = max(os.path.getmtime(f) for f in _MODULE_DEPENDS[path]) |
---|
[839fd68] | 76 | _MODULE_CACHE[path] = module, timestamp |
---|
| 77 | |
---|
[91bd550] | 78 | #print("loading", os.path.basename(path), _MODULE_CACHE[path][1], |
---|
| 79 | # [os.path.basename(p) for p in _MODULE_DEPENDS[path]]) |
---|
| 80 | |
---|
[d321747] | 81 | # Add path and all its dependence to the parent module, if there is one. |
---|
[91bd550] | 82 | if _MODULE_DEPENDS_STACK: |
---|
| 83 | working_on = _MODULE_DEPENDS_STACK[-1] |
---|
| 84 | _MODULE_DEPENDS[working_on].update(_MODULE_DEPENDS[path]) |
---|
| 85 | |
---|
| 86 | return _MODULE_CACHE[path][0] |
---|
| 87 | |
---|
| 88 | def need_reload(path): |
---|
[d321747] | 89 | # type: str -> bool |
---|
| 90 | """ |
---|
| 91 | Return True if any path dependencies have a timestamp newer than the time |
---|
| 92 | when the path was most recently loaded. |
---|
| 93 | """ |
---|
[91bd550] | 94 | # TODO: fails if a dependency has a modification time in the future |
---|
| 95 | # If the newest dependency has a time stamp in the future, then this |
---|
| 96 | # will be recorded as the cached time. When a second dependency |
---|
| 97 | # is updated to the current time stamp, it will still be considered |
---|
| 98 | # older than the current build and the reload will not be triggered. |
---|
| 99 | # Could instead treat all future times as 0 here and in the code above |
---|
| 100 | # which records the newest timestamp. This will force a reload when |
---|
| 101 | # the future time is reached, but other than that should perform |
---|
| 102 | # correctly. Probably not worth the extra code... |
---|
| 103 | _, cache_time = _MODULE_CACHE.get(path, (None, -1)) |
---|
| 104 | depends = _MODULE_DEPENDS.get(path, [path]) |
---|
[d321747] | 105 | #print("reload", any(cache_time < os.path.getmtime(p) for p in depends)) |
---|
| 106 | #for f in depends: print(">>> ", f, os.path.getmtime(f)) |
---|
[91bd550] | 107 | return any(cache_time < os.path.getmtime(p) for p in depends) |
---|
[d321747] | 108 | |
---|
| 109 | def _find_sources(path, source_list): |
---|
| 110 | # type: (str, List[str]) -> List[str] |
---|
| 111 | """ |
---|
| 112 | Return a list of the sources relative to base file; ignore any that |
---|
| 113 | are not found. |
---|
| 114 | """ |
---|
| 115 | root = dirname(path) |
---|
| 116 | found = [] |
---|
| 117 | for source_name in source_list: |
---|
| 118 | source_path = joinpath(root, source_name) |
---|
| 119 | if exists(source_path): |
---|
| 120 | found.append(source_path) |
---|
| 121 | return found |
---|