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 | """ |
---|
10 | from __future__ import division, print_function |
---|
11 | |
---|
12 | import sys |
---|
13 | import os |
---|
14 | from os.path import basename, splitext, join as joinpath, exists, dirname |
---|
15 | |
---|
16 | try: |
---|
17 | # Python 3.5 and up |
---|
18 | from importlib.util import spec_from_file_location, module_from_spec # type: ignore |
---|
19 | def load_module_from_path(fullname, path): |
---|
20 | # type: (str, str) -> "module" |
---|
21 | """load module from *path* as *fullname*""" |
---|
22 | spec = spec_from_file_location(fullname, os.path.expanduser(path)) |
---|
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): |
---|
30 | # type: (str, str) -> "module" |
---|
31 | """load module from *path* as *fullname*""" |
---|
32 | # Clear out old definitions, if any |
---|
33 | if fullname in sys.modules: |
---|
34 | del sys.modules[fullname] |
---|
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") |
---|
38 | module = imp.load_source(fullname, os.path.expanduser(path)) |
---|
39 | return module |
---|
40 | |
---|
41 | _MODULE_CACHE = {} # type: Dict[str, Tuple("module", int)] |
---|
42 | _MODULE_DEPENDS = {} # type: Dict[str, List[str]] |
---|
43 | _MODULE_DEPENDS_STACK = [] # type: List[str] |
---|
44 | def load_custom_kernel_module(path): |
---|
45 | # type: str -> "module" |
---|
46 | """load SAS kernel from *path* as *sasmodels.custom.modelname*""" |
---|
47 | # Pull off the last .ext if it exists; there may be others |
---|
48 | name = basename(splitext(path)[0]) |
---|
49 | path = os.path.expanduser(path) |
---|
50 | |
---|
51 | # Reload module if necessary. |
---|
52 | if need_reload(path): |
---|
53 | # Assume the module file is the only dependency |
---|
54 | _MODULE_DEPENDS[path] = set([path]) |
---|
55 | |
---|
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) |
---|
61 | module = load_module_from_path('sasmodels.custom.'+name, path) |
---|
62 | _MODULE_DEPENDS_STACK.pop() |
---|
63 | |
---|
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)) |
---|
73 | |
---|
74 | # Cache the module, and tag it with the newest timestamp |
---|
75 | timestamp = max(os.path.getmtime(f) for f in _MODULE_DEPENDS[path]) |
---|
76 | _MODULE_CACHE[path] = module, timestamp |
---|
77 | |
---|
78 | #print("loading", os.path.basename(path), _MODULE_CACHE[path][1], |
---|
79 | # [os.path.basename(p) for p in _MODULE_DEPENDS[path]]) |
---|
80 | |
---|
81 | # Add path and all its dependence to the parent module, if there is one. |
---|
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): |
---|
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 | """ |
---|
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]) |
---|
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)) |
---|
107 | return any(cache_time < os.path.getmtime(p) for p in depends) |
---|
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 |
---|